diff --git a/.claude/commands/lint.md b/.claude/commands/lint.md
new file mode 100644
index 0000000..3fb416b
--- /dev/null
+++ b/.claude/commands/lint.md
@@ -0,0 +1,111 @@
+# Python Linter
+
+Run Python code linting and formatting tools.
+
+## Purpose
+
+This command helps you maintain code quality using Python's best linting and formatting tools.
+
+## Usage
+
+```
+/lint
+```
+
+## What this command does
+
+1. **Runs multiple linters** (flake8, pylint, black, isort)
+2. **Provides detailed feedback** on code quality issues
+3. **Auto-fixes formatting** where possible
+4. **Checks type hints** if mypy is configured
+
+## Example Commands
+
+### Black (code formatting)
+```bash
+# Format all Python files
+black .
+
+# Check formatting without changing files
+black --check .
+
+# Format specific file
+black src/main.py
+```
+
+### flake8 (style guide enforcement)
+```bash
+# Check all Python files
+flake8 .
+
+# Check specific directory
+flake8 src/
+
+# Check with specific rules
+flake8 --max-line-length=88 .
+```
+
+### isort (import sorting)
+```bash
+# Sort imports in all files
+isort .
+
+# Check import sorting
+isort --check-only .
+
+# Sort imports in specific file
+isort src/main.py
+```
+
+### pylint (comprehensive linting)
+```bash
+# Run pylint on all files
+pylint src/
+
+# Run with specific score threshold
+pylint --fail-under=8.0 src/
+
+# Generate detailed report
+pylint --output-format=html src/ > pylint_report.html
+```
+
+### mypy (type checking)
+```bash
+# Check types in all files
+mypy .
+
+# Check specific module
+mypy src/models.py
+
+# Check with strict mode
+mypy --strict src/
+```
+
+## Configuration Files
+
+Most projects benefit from configuration files:
+
+### .flake8
+```ini
+[flake8]
+max-line-length = 88
+exclude = .git,__pycache__,venv
+ignore = E203,W503
+```
+
+### pyproject.toml
+```toml
+[tool.black]
+line-length = 88
+
+[tool.isort]
+profile = "black"
+```
+
+## Best Practices
+
+- Run linters before committing code
+- Use consistent formatting across the project
+- Fix linting issues promptly
+- Configure linters to match your team's style
+- Use type hints for better code documentation
\ No newline at end of file
diff --git a/.claude/commands/test.md b/.claude/commands/test.md
new file mode 100644
index 0000000..64cb56c
--- /dev/null
+++ b/.claude/commands/test.md
@@ -0,0 +1,73 @@
+# Test Runner
+
+Run Python tests with pytest, unittest, or other testing frameworks.
+
+## Purpose
+
+This command helps you run Python tests effectively with proper configuration and reporting.
+
+## Usage
+
+```
+/test
+```
+
+## What this command does
+
+1. **Detects test framework** (pytest, unittest, nose2)
+2. **Runs appropriate tests** with proper configuration
+3. **Provides coverage reporting** if available
+4. **Shows clear test results** with failure details
+
+## Example Commands
+
+### pytest (recommended)
+```bash
+# Run all tests
+pytest
+
+# Run with coverage
+pytest --cov=src --cov-report=html
+
+# Run specific test file
+pytest tests/test_models.py
+
+# Run with verbose output
+pytest -v
+
+# Run tests matching pattern
+pytest -k "test_user"
+```
+
+### unittest
+```bash
+# Run all tests
+python -m unittest discover
+
+# Run specific test file
+python -m unittest tests.test_models
+
+# Run with verbose output
+python -m unittest -v
+```
+
+### Django tests
+```bash
+# Run all Django tests
+python manage.py test
+
+# Run specific app tests
+python manage.py test myapp
+
+# Run with coverage
+coverage run --source='.' manage.py test
+coverage report
+```
+
+## Best Practices
+
+- Write tests for all critical functionality
+- Use descriptive test names
+- Keep tests isolated and independent
+- Mock external dependencies
+- Aim for high test coverage (80%+)
\ No newline at end of file
diff --git a/.claude/settings.json b/.claude/settings.json
new file mode 100644
index 0000000..2714f4d
--- /dev/null
+++ b/.claude/settings.json
@@ -0,0 +1,151 @@
+{
+ "permissions": {
+ "allow": [
+ "Bash",
+ "Edit",
+ "MultiEdit",
+ "Write",
+ "Bash(python:*)",
+ "Bash(pytest:*)",
+ "Bash(pip:*)",
+ "Bash(uv:*)",
+ "Bash(uv pip:*)",
+ "Bash(black:*)",
+ "Bash(isort:*)",
+ "Bash(flake8:*)",
+ "Bash(mypy:*)",
+ "Bash(git:*)"
+ ],
+ "deny": [
+ "Bash(curl:*)",
+ "Bash(wget:*)",
+ "Bash(rm -rf:*)"
+ ],
+ "defaultMode": "dontAsk"
+ },
+ "env": {
+ "BASH_DEFAULT_TIMEOUT_MS": "60000",
+ "BASH_MAX_OUTPUT_LENGTH": "20000",
+ "CLAUDE_BASH_MAINTAIN_PROJECT_WORKING_DIR": "1",
+ "PYTHONPATH": "."
+ },
+ "includeCoAuthoredBy": true,
+ "cleanupPeriodDays": 30,
+ "hooks": {
+ "PreToolUse": [
+ {
+ "matcher": "Bash",
+ "hooks": [
+ {
+ "type": "command",
+ "command": "jq -r '\"\\(.tool_input.command) - \\(.tool_input.description // \"No description\")\"' >> ~/.claude/bash-command-log.txt"
+ }
+ ]
+ },
+ {
+ "matcher": "Write",
+ "hooks": [
+ {
+ "type": "command",
+ "command": "FILE=$(echo $STDIN_JSON | jq -r '.tool_input.file_path // \"\"); CONTENT=$(echo $STDIN_JSON | jq -r '.tool_input.content // \"\"); if [[ \"$FILE\" =~ \\.py$ ]] && echo \"$CONTENT\" | grep -q 'print('; then echo 'Warning: print() statements should be replaced with logging' >&2; exit 2; fi"
+ }
+ ]
+ },
+ {
+ "matcher": "Write",
+ "hooks": [
+ {
+ "type": "command",
+ "command": "FILE=$(echo $STDIN_JSON | jq -r '.tool_input.file_path // \"\"'); if [[ \"$FILE\" == \"requirements.txt\" ]] || [[ \"$FILE\" == \"pyproject.toml\" ]] || [[ \"$FILE\" == \"setup.py\" ]]; then echo 'Checking for vulnerable dependencies...'; if command -v safety >/dev/null 2>&1; then safety check; elif command -v pip-audit >/dev/null 2>&1; then pip-audit; else echo 'No security audit tool found. Install safety or pip-audit'; fi; fi",
+ "timeout": 60
+ }
+ ]
+ }
+ ],
+ "PostToolUse": [
+ {
+ "matcher": "Write|Edit|MultiEdit",
+ "hooks": [
+ {
+ "type": "command",
+ "command": "FILE=$(echo $STDIN_JSON | jq -r '.tool_input.file_path // \"\"'); if [[ \"$FILE\" =~ \\.py$ ]]; then black \"$FILE\" 2>/dev/null || echo 'Black formatting skipped (not installed)'; fi",
+ "timeout": 30
+ }
+ ]
+ },
+ {
+ "matcher": "Write|Edit|MultiEdit",
+ "hooks": [
+ {
+ "type": "command",
+ "command": "FILE=$(echo $STDIN_JSON | jq -r '.tool_input.file_path // \"\"'); if [[ \"$FILE\" =~ \\.py$ ]]; then isort \"$FILE\" 2>/dev/null || echo 'isort skipped (not installed)'; fi",
+ "timeout": 30
+ }
+ ]
+ },
+ {
+ "matcher": "Write|Edit|MultiEdit",
+ "hooks": [
+ {
+ "type": "command",
+ "command": "FILE=$(echo $STDIN_JSON | jq -r '.tool_input.file_path // \"\"'); if [[ \"$FILE\" =~ \\.py$ ]]; then RESULT=$(flake8 \"$FILE\" 2>&1); if [ $? -ne 0 ] && command -v flake8 >/dev/null 2>&1; then echo \"Flake8 linting issues found: $RESULT\" >&2; exit 2; fi; fi",
+ "timeout": 30
+ }
+ ]
+ },
+ {
+ "matcher": "Write|Edit|MultiEdit",
+ "hooks": [
+ {
+ "type": "command",
+ "command": "FILE=$(echo $STDIN_JSON | jq -r '.tool_input.file_path // \"\"'); if [[ \"$FILE\" =~ \\.py$ ]]; then RESULT=$(mypy \"$FILE\" 2>&1); if [ $? -ne 0 ] && command -v mypy >/dev/null 2>&1; then echo \"MyPy type checking issues found: $RESULT\" >&2; exit 2; fi; fi",
+ "timeout": 30
+ }
+ ]
+ },
+ {
+ "matcher": "Write|Edit|MultiEdit",
+ "hooks": [
+ {
+ "type": "command",
+ "command": "FILE=$(echo $STDIN_JSON | jq -r '.tool_input.file_path // \"\"'); if [[ \"$FILE\" =~ \\.py$ && \"$FILE\" != *\"test_\"* && \"$FILE\" != *\"_test.py\" ]]; then DIR=$(dirname \"$FILE\"); BASENAME=$(basename \"$FILE\" .py); for TEST_FILE in \"$DIR/test_$BASENAME.py\" \"$DIR/${BASENAME}_test.py\" \"tests/test_$BASENAME.py\"; do if [ -f \"$TEST_FILE\" ]; then echo \"Running tests for $TEST_FILE...\"; if command -v pytest >/dev/null 2>&1; then pytest \"$TEST_FILE\" -v; elif python -m pytest \"$TEST_FILE\" 2>/dev/null; then python -m pytest \"$TEST_FILE\" -v; else python -m unittest \"$TEST_FILE\" 2>/dev/null || echo 'No test runner found'; fi; break; fi; done; fi",
+ "timeout": 60
+ }
+ ]
+ }
+ ],
+ "Notification": [
+ {
+ "matcher": "",
+ "hooks": [
+ {
+ "type": "command",
+ "command": "echo \"Claude Code notification: $(date)\" >> ~/.claude/notifications.log"
+ }
+ ]
+ }
+ ],
+ "Stop": [
+ {
+ "matcher": "",
+ "hooks": [
+ {
+ "type": "command",
+ "command": "if [[ -f requirements.txt || -f pyproject.toml || -f setup.py ]] && [[ $(git status --porcelain | grep '\\.py$') ]]; then echo 'Running linter on changed Python files...'; if command -v flake8 >/dev/null 2>&1; then flake8 $(git diff --name-only --diff-filter=ACMR | grep '\\.py$'); elif command -v pylint >/dev/null 2>&1; then pylint $(git diff --name-only --diff-filter=ACMR | grep '\\.py$'); else echo 'No Python linter found (flake8/pylint)'; fi; fi",
+ "timeout": 60
+ }
+ ]
+ },
+ {
+ "matcher": "",
+ "hooks": [
+ {
+ "type": "command",
+ "command": "if [[ -f requirements.txt || -f pyproject.toml || -f setup.py ]] && [[ $(git status --porcelain | grep '\\.py$') ]]; then echo 'Running type checking on changed files...'; if command -v mypy >/dev/null 2>&1; then mypy $(git diff --name-only --diff-filter=ACMR | grep '\\.py$') || echo 'Type checking completed with issues'; else echo 'MyPy not found for type checking'; fi; fi",
+ "timeout": 60
+ }
+ ]
+ }
+ ]
+ }
+}
diff --git a/.env.example b/.env.example
index 8b81cc8..14e08a9 100644
--- a/.env.example
+++ b/.env.example
@@ -2,5 +2,7 @@
API_ID=123456
API_HASH=your_api_hash_here
-# Optional: session file name (default: session)
+# Optional: session file name or path
+# - Nome simples (ex.: session) salva em ~/.clean_telegram/
+# - Caminho absoluto/relativo permite personalizar local
SESSION_NAME=session
diff --git a/.flake8 b/.flake8
new file mode 100644
index 0000000..4d5d05f
--- /dev/null
+++ b/.flake8
@@ -0,0 +1,13 @@
+[flake8]
+max-line-length = 100
+exclude =
+ .git,
+ __pycache__,
+ .venv,
+ venv,
+ build,
+ dist,
+ *.egg-info
+ignore = E203, W503
+per-file-ignores =
+ __init__.py:F401
diff --git a/.gitignore b/.gitignore
index 68f77c5..5728deb 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,6 +1,151 @@
-.venv/
+# Byte-compiled / optimized / DLL files
__pycache__/
-*.pyc
+*.py[cod]
+*$py.class
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+build/
+develop-eggs/
+dist/
+downloads/
+eggs/
+.eggs/
+lib/
+lib64/
+parts/
+sdist/
+var/
+wheels/
+share/python-wheels/
+*.egg-info/
+.installed.cfg
+*.egg
+MANIFEST
+
+# PyInstaller
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov/
+.tox/
+.nox/
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*.cover
+*.py,cover
+.hypothesis/
+.pytest_cache/
+cache/
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+local_settings.py
+db.sqlite3
+db.sqlite3-journal
+
+# Flask stuff:
+instance/
+.webassets-cache
+
+# Scrapy stuff:
+.scrapy
+
+# Sphinx documentation
+docs/_build/
+
+# PyBuilder
+.pybuilder/
+target/
+
+# Jupyter Notebook
+.ipynb_checkpoints
+
+# IPython
+profile_default/
+ipython_config.py
+
+# pyenv
+.python-version
+
+# pipenv
+Pipfile.lock
+
+# poetry
+poetry.lock
+
+# PEP 582
+__pypackages__/
+
+# Celery stuff
+celerybeat-schedule
+celerybeat.pid
+
+# SageMath parsed files
+*.sage.py
+
+# Environments
.env
+.venv
+env/
+venv/
+ENV/
+env.bak/
+venv.bak/
+
+# Spyder project settings
+.spyderproject
+.spyproject
+
+# Rope project settings
+.ropeproject
+
+# mkdocs documentation
+/site
+
+# mypy
+.mypy_cache/
+.dmypy.json
+dmypy.json
+
+# Pyre type checker
+.pyre/
+
+# pytype static type analyzer
+.pytype/
+
+# Cython debug symbols
+cython_debug/
+
+# PyCharm
+.idea/
+
+# VSCode
+.vscode/
+
+# Telegram session files
*.session
*.session-journal
+
+# OS
+.DS_Store
+Thumbs.db
+
+# Debug files
+*_debug.md
+.debug*
diff --git a/.mcp.json b/.mcp.json
new file mode 100644
index 0000000..a0944ce
--- /dev/null
+++ b/.mcp.json
@@ -0,0 +1,78 @@
+{
+ "mcpServers": {
+ "python-sdk": {
+ "name": "Python SDK",
+ "description": "Official Python SDK with FastMCP for rapid MCP development",
+ "command": "python",
+ "args": ["-m", "python_sdk.server"],
+ "env": {}
+ },
+ "docker": {
+ "name": "Docker MCP",
+ "description": "Isolated code execution via Docker containers",
+ "command": "python",
+ "args": ["-m", "mcp_server_docker"],
+ "env": {}
+ },
+ "jupyter": {
+ "name": "Jupyter MCP",
+ "description": "MCP integration for interactive Jupyter notebooks",
+ "command": "python",
+ "args": ["-m", "server_jupyter"],
+ "env": {}
+ },
+ "postgresql": {
+ "name": "PostgreSQL MCP",
+ "description": "Natural language queries to PostgreSQL databases",
+ "command": "python",
+ "args": ["-m", "server_postgres"],
+ "env": {
+ "DATABASE_URL": ""
+ }
+ },
+ "opik": {
+ "name": "Opik MCP",
+ "description": "Observability for LLM apps with tracing and metrics",
+ "command": "python",
+ "args": ["-m", "opik_mcp"],
+ "env": {}
+ },
+ "memory-bank": {
+ "name": "Memory Bank MCP",
+ "description": "Centralized memory system for AI agents",
+ "command": "server-memory",
+ "args": [],
+ "env": {}
+ },
+ "sequential-thinking": {
+ "name": "Sequential Thinking MCP",
+ "description": "Helps LLMs decompose complex tasks into logical steps",
+ "command": "code-reasoning",
+ "args": [],
+ "env": {}
+ },
+ "brave-search": {
+ "name": "Brave Search MCP",
+ "description": "Privacy-focused web search tool",
+ "command": "server-brave-search",
+ "args": [],
+ "env": {}
+ },
+ "google-maps": {
+ "name": "Google Maps MCP",
+ "description": "Integrates Google Maps for geolocation and directions",
+ "command": "server-google-maps",
+ "args": [],
+ "env": {
+ "GOOGLE_MAPS_API_KEY": ""
+ }
+ },
+ "deep-graph": {
+ "name": "Deep Graph MCP (Code Graph)",
+ "description": "Transforms source code into semantic graphs via DeepGraph",
+ "command": "mcp-code-graph",
+ "args": [],
+ "env": {}
+ }
+ }
+}
\ No newline at end of file
diff --git a/CLAUDE.md b/CLAUDE.md
new file mode 100644
index 0000000..62d8476
--- /dev/null
+++ b/CLAUDE.md
@@ -0,0 +1,259 @@
+# CLAUDE.md
+
+Este arquivo fornece orientação para o Claude Code (claude.ai/code) ao trabalhar com código neste repositório.
+
+## Project Overview
+
+CleanTelegram é um projeto Python que automatiza a limpeza de contas Telegram usando a biblioteca Telethon. O projeto segue práticas modernas de desenvolvimento Python com estrutura baseada em `src/` e ferramentas de qualidade configuradas.
+
+## Development Commands
+
+### Environment Management (UV)
+Recomendamos usar **UV** como gerenciador de pacotes para este projeto:
+
+```bash
+# Instalar UV (se ainda não tiver)
+curl -LsSf https://astral.sh/uv/install.sh | sh
+
+# Criar ambiente virtual com UV
+uv venv
+
+# Ativar ambiente
+source .venv/bin/activate # Linux/Mac
+# ou
+.venv\Scripts\activate # Windows
+
+# Instalar dependências
+uv pip install -e ".[dev]"
+
+# Ou sem UV:
+pip install -e ".[dev]"
+```
+
+### Running the Application
+
+```bash
+# Executar com módulo Python
+python -m clean_telegram --help
+
+# Dry-run (testar sem alterações)
+python -m clean_telegram --dry-run
+
+# Executar com limitação de diálogos
+python -m clean_telegram --limit 10
+
+# Execução completa (requer confirmação)
+python -m clean_telegram
+
+# Execução sem confirmação
+python -m clean_telegram --yes
+```
+
+### Testing Commands
+
+```bash
+# pytest - Run all tests
+pytest
+
+# Run with coverage
+pytest --cov=src/clean_telegram --cov-report=html
+
+# Run specific test file
+pytest tests/test_client.py
+
+# Run with verbose output
+pytest -v
+
+# Run tests matching pattern
+pytest -k "test_delete"
+```
+
+### Code Quality Commands
+
+```bash
+# Format code with Black
+black src/ tests/
+
+# Check formatting without changes
+black --check src/ tests/
+
+# Sort imports
+isort src/ tests/
+
+# Check import sorting
+isort --check-only src/ tests/
+
+# Run linting with Flake8
+flake8 src/ tests/
+
+# Type checking with MyPy
+mypy src/
+
+# Run all quality checks at once
+black --check src/ tests/ && isort --check-only src/ tests/ && flake8 src/ tests/ && mypy src/
+```
+
+## Technology Stack
+
+### Core Technologies
+- **Python 3.10+** - Linguagem primária
+- **Telethon** - Biblioteca para interagir com Telegram MTProto API
+- **python-dotenv** - Gerenciamento de variáveis de ambiente
+
+### Development Tools
+- **UV** - Gerenciador de pacotes (recomendado)
+- **pytest** - Framework de testes
+- **pytest-asyncio** - Suporte para testes assíncronos
+- **pytest-cov** - Relatório de cobertura de testes
+
+### Code Quality Tools
+- **Black** - Formatador de código
+- **isort** - Ordenador de imports
+- **Flake8** - Linter (guia de estilo PEP 8)
+- **MyPy** - Verificador de tipos estáticos
+
+## Project Structure
+
+```
+CleanTelegram/
+├── src/
+│ └── clean_telegram/
+│ ├── __init__.py # Pacote principal com exports
+│ ├── __main__.py # Entry-point do CLI
+│ ├── client.py # Funções de interação com Telegram
+│ └── utils.py # Funções utilitárias
+├── tests/
+│ ├── __init__.py
+│ ├── conftest.py # Configuração pytest
+│ ├── test_client.py # Testes do módulo client
+│ └── test_utils.py # Testes do módulo utils
+├── .env.example # Exemplo de variáveis de ambiente
+├── .flake8 # Configuração do Flake8
+├── pyproject.toml # Configuração do projeto (moderno)
+├── requirements.txt # Dependências de produção
+├── requirements-dev.txt # Dependências de desenvolvimento
+├── README.md # Documentação do projeto
+└── CLAUDE.md # Este arquivo
+```
+
+## Naming Conventions
+- **Files/Modules**: `snake_case` (ex: `client.py`, `test_utils.py`)
+- **Classes**: `PascalCase` (ex: `TelegramClient`)
+- **Functions/Variables**: `snake_case` (ex: `process_dialog`, `env_int`)
+- **Constants**: `UPPER_SNAKE_CASE` (ex: `API_ID`)
+- **Private methods**: Prefixo `_` (ex: `_private_method`)
+
+## Python Guidelines
+
+### Type Hints
+Use type hints para parâmetros de função e valores de retorno:
+```python
+async def process_dialog(
+ client: TelegramClient,
+ entity: Union[Channel, Chat, User],
+ title: str,
+ index: int,
+ *,
+ dry_run: bool,
+) -> bool:
+ """Processa um diálogo do Telegram."""
+```
+
+### Code Style
+- Siga PEP 8
+- Limite de linha: 100 caracteres
+- Use docstrings para módulos, classes e funções
+- Funções devem ter propósito único
+- Use `logging` em vez de `print`
+
+### Best Practices
+- Use `pathlib` para operações com arquivos
+- Use context managers (`with`) para gerenciamento de recursos
+- Trate exceções apropriadamente com try/except
+- Use `asyncio` para operações I/O pesadas (Telethon é assíncrono)
+
+## Testing Standards
+
+### Test Structure
+- Organize testes espelhando a estrutura do código fonte
+- Use nomes descritivos para testes
+- Siga padrão AAA (Arrange, Act, Assert)
+- Use fixtures para dados comuns de teste
+
+### Coverage Goals
+- Objetivo: 70%+ de cobertura
+- Testes unitários para lógica de negócio
+- Testes assíncronos para funções Telethon
+
+### pytest Configuration
+Configuração está em `pyproject.toml`:
+```toml
+[tool.pytest.ini_options]
+testpaths = ["tests"]
+asyncio_mode = "auto"
+```
+
+## Environment Setup
+
+### Installation with UV
+```bash
+# Clonar repositório
+git clone https://github.com/gabrielramos/CleanTelegram
+cd CleanTelegram
+
+# Criar venv com UV
+uv venv
+source .venv/bin/activate
+
+# Instalar dependências
+uv pip install -e ".[dev]"
+
+# Configurar ambiente
+cp .env.example .env
+# Editar .env com API_ID e API_HASH
+```
+
+### Environment Variables
+- `API_ID`: ID da API do Telegram (obrigatório)
+- `API_HASH`: Hash da API do Telegram (obrigatório)
+- `SESSION_NAME`: Nome da sessão Telethon (opcional, padrão: "session")
+
+## Security Guidelines
+
+- Nunca commite `.env` com credenciais reais
+- Nunca commite arquivos `*.session` - contêm credenciais de autenticação do Telegram
+- Use `.env.example` como template
+- Valide input do usuário
+- Trate exceções de API apropriadamente (FloodWaitError, RPCError)
+- Use logs em vez de print para debug
+
+## Development Workflow
+
+### Before Starting
+1. Ative o ambiente virtual
+2. Instale dependências: `uv pip install -e ".[dev]"`
+3. Configure `.env` com credenciais
+
+### During Development
+1. Use type hints para melhor documentação
+2. Execute testes frequentemente
+3. Use mensagens de commit significativas
+4. Formate código com Black antes de commitar
+
+### Before Committing
+1. `pytest` - Execute testes
+2. `black --check src/ tests/` - Verifique formatação
+3. `isort --check-only src/ tests/` - Verifique imports
+4. `flake8 src/ tests/` - Verifique lint
+5. `mypy src/` - Verifique tipos
+
+## CLI Reference
+
+```bash
+python -m clean_telegram --help
+```
+
+Opções disponíveis:
+- `--dry-run`: Não faz alterações, só mostra o que faria
+- `--yes`: Não pede confirmação interativa
+- `--limit N`: Limita a N diálogos processados (0 = todos)
diff --git a/README.md b/README.md
index 3ead001..71bccbd 100644
--- a/README.md
+++ b/README.md
@@ -1,55 +1,223 @@
# CleanTelegram
-**Tags:** telegram, telethon, python, cleanup, automation, privacy, destructive
+> Automação para limpar sua conta Telegram: apaga conversas, sai de grupos e canais.
-**Atenção:** este projeto automatiza ações destrutivas na sua conta Telegram (apagar conversas e sair de grupos/canais). Use **por sua conta e risco**. Recomendo testar primeiro com `--dry-run`.
+**Tags:** `telegram` `telethon` `python` `cleanup` `automation` `privacy`
-## O que faz
+---
-- Apaga diálogos (conversas) com usuários/bots.
-- Sai de **grupos** e **canais**.
-- (Opcional) arquiva e silencia o que não dá para “bloquear”.
+> **ATENÇÃO — AÇÃO DESTRUTIVA**
+>
+> Este script **apaga conversas** e **sai de grupos/canais** da sua conta Telegram.
+> As ações são **irreversíveis**. Teste sempre com `--dry-run` antes de executar de verdade.
-> Observação: Telegram não tem um “bloquear grupo” de verdade (bloqueio é para **usuários**). Para grupos/canais, o equivalente prático é **sair**; e/ou **arquivar + silenciar**.
+---
+
+## Funcionalidades
+
+- Apaga histórico de conversas com **usuários** e **bots** (com revogação quando possível)
+- Sai de **canais** e **megagrupos** (`LeaveChannelRequest`)
+- Sai de **grupos legados** (`DeleteChatUserRequest`) com fallback automático
+- Modo **`--dry-run`** — simula a execução sem alterar nada
+- Confirmação obrigatória **`"APAGAR TUDO"`** para evitar execução acidental
+- **Rate-limit handling** — retry automático com backoff em caso de `FloodWaitError`
+- Delay entre operações (`0.35s`) para proteger contra bloqueio temporário da API
+
+> **Nota:** O Telegram não oferece "bloqueio" de grupos/canais — apenas de usuários.
+> Para grupos e canais, o equivalente prático é **sair**.
## Requisitos
-- Python 3.10+
-- Credenciais do Telegram API: `API_ID` e `API_HASH`
- - Pegue em: https://my.telegram.org
+- **Python 3.10** ou superior
+- Credenciais da **Telegram API**: `API_ID` e `API_HASH`
+ - Obtenha em: [https://my.telegram.org](https://my.telegram.org)
## Instalação
```bash
+# 1. Clonar o repositório
+git clone https://github.com/prof-ramos/CleanTelegram.git
+cd CleanTelegram
+
+# 2. Criar e ativar virtualenv
python -m venv .venv
-source .venv/bin/activate
+source .venv/bin/activate # Linux / macOS
+# .venv\Scripts\activate # Windows
+
+# 3. Instalar dependências
pip install -r requirements.txt
+
+# 4. Configurar variáveis de ambiente
cp .env.example .env
```
-Edite `.env` com `API_ID` e `API_HASH`.
+Edite o arquivo `.env` com suas credenciais:
+
+```ini
+API_ID=123456
+API_HASH=abc123def456...
+SESSION_NAME=session # opcional; padrão persiste em ~/.clean_telegram/
+```
## Uso
-Dry-run (recomendado):
+### Modo seguro (dry-run) — recomendado para começar
+
+Simula a execução e mostra o que **seria** feito, sem alterar nada:
```bash
python clean_telegram.py --dry-run
```
-Executar de verdade:
+Saída esperada:
+
+```
+2025-01-15 10:30:00 INFO: Logado como: @seuuser (id=123456789)
+2025-01-15 10:30:01 INFO: [1] SAIR de canal/megagrupo: Canal Exemplo
+2025-01-15 10:30:01 INFO: [2] APAGAR conversa: João Silva
+2025-01-15 10:30:01 INFO: [3] SAIR de grupo legado (Chat): Grupo Antigo
+...
+2025-01-15 10:30:02 INFO: Concluído. Diálogos processados: 3
+```
+
+### Execução real
+
+Pede confirmação interativa antes de executar:
```bash
python clean_telegram.py
```
-Opções úteis:
+```
+ATENÇÃO: isso vai apagar conversas e sair de grupos/canais.
+Digite 'APAGAR TUDO' para confirmar: APAGAR TUDO
+```
+
+### Limitar quantidade de diálogos
+
+Processa apenas os primeiros N diálogos (útil para testar de forma gradual):
```bash
-python clean_telegram.py --help
+python clean_telegram.py --limit 5
+```
+
+### Pular confirmação interativa
+
+Para uso em scripts ou automação (usar com cuidado):
+
+```bash
+python clean_telegram.py --yes
+```
+
+## Referência de flags
+
+| Flag | Descrição | Padrão |
+|------|-----------|--------|
+| `--dry-run` | Simula a execução sem alterar nada | Desativado |
+| `--yes` | Pula a confirmação interativa `"APAGAR TUDO"` | Desativado |
+| `--limit N` | Processa no máximo N diálogos (`0` = todos) | `0` |
+| `--help` | Exibe ajuda com todas as opções | — |
+
+## Variáveis de ambiente
+
+| Variável | Obrigatória | Descrição |
+|----------|:-----------:|-----------|
+| `API_ID` | Sim | ID da aplicação Telegram (inteiro) |
+| `API_HASH` | Sim | Hash da aplicação Telegram (string hex) |
+| `SESSION_NAME` | Não | Nome/caminho da sessão. Nome simples usa `~/.clean_telegram/`; caminho absoluto/relativo usa o caminho informado |
+
+## Como funciona
+
+O script segue um fluxo linear assíncrono:
+
```
+main()
+ │
+ ├─ Carrega .env e valida credenciais
+ ├─ Pede confirmação "APAGAR TUDO" (se necessário)
+ ├─ Conecta ao Telegram via TelegramClient
+ │
+ └─ Para cada diálogo:
+ │
+ ├─ Channel (canal/megagrupo) → LeaveChannelRequest
+ ├─ Chat (grupo legado) → DeleteChatUserRequest
+ │ └─ fallback: client.delete_dialog()
+ ├─ User / Bot → DeleteHistoryRequest
+ └─ Tipo desconhecido → client.delete_dialog()
+ │
+ └─ safe_sleep(0.35s)
+```
+
+### Tratamento de erros
+
+| Erro | Comportamento |
+|------|---------------|
+| `FloodWaitError` | Retry automático (até 5 tentativas), aguardando o tempo indicado pela API |
+| `RPCError` | Loga o erro com traceback e pula para o próximo diálogo |
+| `Exception` genérica | Loga o erro completo e pula para o próximo diálogo |
+
+## Primeira execução
+
+Na primeira vez que rodar o script, o Telethon solicitará:
+
+1. **Número de telefone** — no formato internacional (ex.: `+5511999999999`)
+2. **Código de verificação** — recebido no próprio Telegram
+3. **Senha 2FA** — se autenticação em duas etapas estiver ativada
+
+Após o login, um arquivo de sessão (ex.: `~/.clean_telegram/session.session`) é criado e reaproveitado nas próximas execuções, evitando pedir telefone/código novamente.
+
+> **Segurança:** O arquivo `*.session` contém credenciais de autenticação.
+> Ele está no `.gitignore` e **nunca** deve ser commitado ou compartilhado.
+
+## Estrutura do projeto
+
+```
+CleanTelegram/
+├── clean_telegram.py # Script principal (ponto de entrada único)
+├── requirements.txt # Dependências (telethon, python-dotenv)
+├── .env.example # Template de variáveis de ambiente
+├── .gitignore # Ignora .venv, .env, *.session, __pycache__
+├── CLAUDE.md # Guia para assistentes de IA
+├── docs/
+│ └── UX_FLOW.md # Fluxo UX detalhado com diagramas
+└── README.md # Este arquivo
+```
+
+## Dependências
+
+| Pacote | Versão | Finalidade |
+|--------|--------|------------|
+| [Telethon](https://github.com/LonamiWebs/Telethon) | 1.42.0 | Cliente Telegram (MTProto) |
+| [python-dotenv](https://github.com/theskumar/python-dotenv) | 1.2.1 | Carregamento de variáveis do `.env` |
+
+## Perguntas frequentes
+
+
+Posso recuperar conversas apagadas?
+
+Não. A ação é irreversível. O script usa `DeleteHistoryRequest` com `revoke=True`, que apaga o histórico para ambos os lados quando possível. Use sempre `--dry-run` antes.
+
+
+
+O script pode bloquear minha conta?
+
+O script inclui delays entre operações (`0.35s`) e retry automático em caso de `FloodWaitError` para minimizar esse risco. Ainda assim, executar em contas com muitos diálogos pode gerar rate limits temporários do Telegram. Use `--limit` para processar em lotes menores.
+
+
+
+E se eu receber "FloodWaitError"?
+
+O script trata automaticamente: aguarda o tempo exigido pela API e tenta novamente (até 5 vezes). Se persistir, o diálogo é pulado e o script continua. Você pode re-executar depois para processar os diálogos restantes.
+
+
+
+Posso escolher quais conversas apagar?
+
+Atualmente não. O script processa **todos** os diálogos da conta (ou os primeiros N com `--limit`). Filtragem seletiva ainda não foi implementada.
+
-## Notas
+
+Como deslogar / trocar de conta?
-- Na primeira execução, o Telethon vai pedir o **número** e o **código** (e 2FA, se houver) e salvará uma sessão local em `session.session`.
-- Pode haver limitações/erros por rate limit do Telegram; o script tenta ser cuidadoso.
+Delete o arquivo de sessão usado (por padrão `~/.clean_telegram/session.session`, ou o caminho definido em `SESSION_NAME`). Na próxima execução, o Telethon pedirá login novamente.
+
diff --git a/clean_telegram.py b/clean_telegram.py.legacy
similarity index 100%
rename from clean_telegram.py
rename to clean_telegram.py.legacy
diff --git a/docs/UX_FLOW.md b/docs/UX_FLOW.md
new file mode 100644
index 0000000..4947bdd
--- /dev/null
+++ b/docs/UX_FLOW.md
@@ -0,0 +1,385 @@
+# Fluxo UX — CleanTelegram
+
+Documento que mapeia a jornada completa do usuário ao interagir com o **CleanTelegram**, um projeto Python moderno para limpeza de contas Telegram via Telethon.
+
+---
+
+## 1. Visão geral dos fluxos
+
+```text
+┌─────────────────────────────────────────────────────┐
+│ JORNADA DO USUÁRIO │
+│ │
+│ ┌──────────┐ ┌──────────┐ ┌────────────────┐ │
+│ │ Setup │──▶│Execução │──▶│ Processamento │ │
+│ │ Inicial │ │ & Auth │ │ de Diálogos │ │
+│ └──────────┘ └──────────┘ └────────────────┘ │
+│ │
+│ 3 modos de execução: │
+│ --dry-run Simulação segura (nenhuma alteração) │
+│ --yes Execução sem confirmação interativa │
+│ (padrão) Execução com confirmação "APAGAR TUDO"│
+└─────────────────────────────────────────────────────┘
+```
+
+---
+
+## 2. Fluxo de Setup Inicial (primeira vez)
+
+### 2.1. Setup com UV (recomendado)
+
+```text
+ USUÁRIO SISTEMA
+ ─────── ──────
+ │
+ │ uv venv
+ │ source .venv/bin/activate
+ │ uv pip install -e ".[dev]"
+ ├──────────────────────────────────▶ Instala telethon + python-dotenv
+ │ + ferramentas de dev
+ │
+ │ cp .env.example .env
+ │ (edita .env com API_ID e API_HASH)
+ ├──────────────────────────────────▶ Configura credenciais
+ │
+ │ python -m clean_telegram --dry-run
+ ├──────────────────────────────────▶ Primeira execução
+ │ │
+ │ ◀── Telethon pede telefone ───────┤
+ │ Digita +55 11 9xxxx-xxxx │
+ ├──────────────────────────────────▶ │
+ │ │
+ │ ◀── Telethon pede código ─────────┤
+ │ Digita código recebido no Telegram│
+ ├──────────────────────────────────▶ │
+ │ │
+ │ ◀── (se 2FA) pede senha ──────────┤
+ │ Digita senha 2FA │
+ ├──────────────────────────────────▶ │
+ │ │
+ │ ├──▶ Salva ~/.clean_telegram/session.session
+ │ │
+ │ ◀── Dry-run: lista diálogos ──────┤
+ │ (nenhuma alteração feita) │
+ ▼ ▼
+```
+
+### 2.2. Setup com pip tradicional
+
+```bash
+# Criar e ativar virtualenv
+python -m venv .venv
+source .venv/bin/activate
+
+# Instalar dependências
+pip install -e ".[dev]"
+
+# Configurar ambiente
+cp .env.example .env
+# Editar .env com API_ID e API_HASH (https://my.telegram.org)
+
+# Executar
+python -m clean_telegram --dry-run
+```
+
+> **Nota:** Após o primeiro login, o arquivo de sessão (por padrão `~/.clean_telegram/session.session`) é reutilizado automaticamente. O fluxo de autenticação não se repete.
+
+---
+
+## 3. Estrutura do Projeto
+
+```text
+CleanTelegram/
+├── src/
+│ └── clean_telegram/
+│ ├── __init__.py # Pacote principal
+│ ├── __main__.py # Entry-point do CLI
+│ ├── client.py # Funções de interação com Telegram
+│ └── utils.py # Funções utilitárias
+├── tests/
+│ ├── __init__.py
+│ ├── conftest.py # Fixtures pytest
+│ ├── test_client.py # Testes do módulo client
+│ └── test_utils.py # Testes do módulo utils
+├── docs/
+│ └── UX_FLOW.md # Este documento
+├── .env.example # Template de variáveis de ambiente
+├── pyproject.toml # Configuração do projeto
+├── requirements.txt # Dependências de produção
+├── requirements-dev.txt # Dependências de desenvolvimento
+└── README.md # Documentação do projeto
+```
+
+---
+
+## 4. Fluxo Principal de Execução
+
+```mermaid
+flowchart TD
+ A[python -m clean_telegram] --> B{Variáveis .env ok?}
+ B -- "API_ID ou API_HASH faltando" --> C[/"SystemExit: Faltou ... no .env"/]
+ B -- "OK" --> D{Qual modo?}
+
+ D -- "--dry-run" --> G[Conecta ao Telegram]
+ D -- "--yes" --> G
+ D -- "padrão" --> E["Exibe: Digite 'APAGAR TUDO' para confirmar"]
+
+ E --> F{Usuário digitou\n'APAGAR TUDO'?}
+ F -- "Não" --> F1[/"Cancelado."/]
+ F -- "Sim" --> G
+
+ G --> H[Login via TelegramClient]
+ H --> I["Log: Logado como @username"]
+ I --> J[Itera sobre diálogos]
+
+ J --> K{Limite atingido?\n--limit N}
+ K -- "Sim" --> L["Log: Concluído. Diálogos processados: N"]
+ K -- "Não / sem limite" --> M[Próximo diálogo]
+
+ M --> N[process_dialog]
+ N --> O{Tipo da entidade}
+
+ O -- "Channel" --> P["SAIR de canal/megagrupo\nLeaveChannelRequest"]
+ O -- "Chat" --> Q["SAIR de grupo legado\nDeleteChatUserRequest"]
+ O -- "User / Bot" --> R["APAGAR conversa\nDeleteHistoryRequest"]
+ O -- "Desconhecido" --> S["APAGAR diálogo\nclient.delete_dialog()"]
+
+ Q -- "RPCError" --> Q1["Fallback:\nclient.delete_dialog()"]
+
+ P --> T["safe_sleep(0.35s)"]
+ Q --> T
+ Q1 --> T
+ R --> T
+ S --> T
+
+ T --> J
+
+ style C fill:#d32f2f,color:#fff
+ style F1 fill:#f57c00,color:#fff
+ style L fill:#388e3c,color:#fff
+```
+
+---
+
+## 5. Fluxo de Tratamento de Erros (por diálogo)
+
+```mermaid
+flowchart TD
+ A[process_dialog] --> B{Resultado}
+
+ B -- "Sucesso" --> C["safe_sleep(0.35s)\n→ próximo diálogo"]
+
+ B -- "FloodWaitError" --> D{Tentativa < 5?}
+ D -- "Sim" --> E["Log: Rate limit. Aguardando Xs...\nawait asyncio.sleep(wait_s)"]
+ E --> A
+ D -- "Não (5 tentativas)" --> F["Log: Max retries atingido\n→ pula diálogo"]
+
+ B -- "RPCError" --> G["Log: RPCError + traceback\n→ pula diálogo"]
+
+ B -- "Exception genérica" --> H["Log: Erro inesperado + traceback\n→ pula diálogo"]
+
+ F --> I[Próximo diálogo]
+ G --> I
+ H --> I
+ C --> I
+
+ style C fill:#388e3c,color:#fff
+ style F fill:#d32f2f,color:#fff
+ style G fill:#f57c00,color:#fff
+ style H fill:#f57c00,color:#fff
+```
+
+---
+
+## 6. Fluxo do modo `--dry-run`
+
+```text
+┌─────────────────────────────────────────────────────────┐
+│ MODO DRY-RUN │
+│ │
+│ Tudo funciona igual ao modo real, EXCETO: │
+│ │
+│ ✓ Confirmação "APAGAR TUDO" é IGNORADA (não pede) │
+│ ✓ Diálogos são iterados normalmente │
+│ ✓ Tipo de cada entidade é identificado │
+│ ✓ Logs são emitidos (SAIR / APAGAR) │
+│ ✗ Nenhuma request destrutiva é enviada ao Telegram │
+│ ✗ LeaveChannelRequest → NÃO executado │
+│ ✗ DeleteChatUserRequest → NÃO executado │
+│ ✗ DeleteHistoryRequest → NÃO executado │
+│ ✗ client.delete_dialog() → NÃO executado │
+│ │
+│ O usuário vê exatamente o que SERIA feito. │
+└─────────────────────────────────────────────────────────┘
+```
+
+---
+
+## 7. Mapa de decisões do roteador `process_dialog`
+
+```text
+ entity recebida
+ │
+ ▼
+ ┌─────────────┐ Sim ┌───────────────────────┐
+ │ é Channel? │────────────▶│ leave_channel() │
+ └──────┬──────┘ │ LeaveChannelRequest │
+ │ Não └───────────────────────┘
+ ▼
+ ┌─────────────┐ Sim ┌───────────────────────┐
+ │ é Chat? │────────────▶│ leave_legacy_chat() │
+ └──────┬──────┘ │ DeleteChatUserRequest │
+ │ Não │ │ │
+ │ │ RPCError? │
+ │ │ ▼ Sim │
+ │ │ client.delete_dialog()│
+ │ └───────────────────────┘
+ ▼
+ ┌─────────────┐ Sim ┌───────────────────────┐
+ │ é User/Bot? │────────────▶│ delete_dialog() │
+ └──────┬──────┘ │ DeleteHistoryRequest │
+ │ Não └───────────────────────┘
+ ▼
+ ┌─────────────────┐ ┌───────────────────────┐
+ │ Tipo │────────▶│ client.delete_dialog() │
+ │ desconhecido │ │ (fallback genérico) │
+ └─────────────────┘ └───────────────────────┘
+```
+
+---
+
+## 8. Tabela de estados do terminal (o que o usuário vê)
+
+| Fase | Saída no terminal | Origem |
+|------|-------------------|--------|
+| Credenciais ausentes | `Faltou API_ID no .env` | `env_int()` / `main()` |
+| Confirmação | `ATENÇÃO: isso vai apagar conversas...` | `main()` via `print()` |
+| Cancelado | `Cancelado.` | `main()` via `print()` |
+| Login | `Logado como: @user (id=123)` | `logger.info` |
+| Canal/megagrupo | `[1] SAIR de canal/megagrupo: NomeCanal` | `logger.info` |
+| Grupo legado | `[2] SAIR de grupo legado (Chat): NomeGrupo` | `logger.info` |
+| Conversa user/bot | `[3] APAGAR conversa: NomeUsuario` | `logger.info` |
+| Tipo desconhecido | `[4] APAGAR diálogo (tipo desconhecido): ...` | `logger.info` |
+| Rate limit | `Rate limit (FloodWait)... Aguardando Xs` | `logger.warning` |
+| Max retries | `Max retries atingido; pulando 'NomeDialogo'` | `logger.error` |
+| Erro RPC | `RPCError em 'NomeDialogo'` | `logger.exception` |
+| Erro genérico | `Erro inesperado em 'NomeDialogo'` | `logger.exception` |
+| Conclusão | `Concluído. Diálogos processados: N` | `logger.info` |
+
+---
+
+## 9. Cenários de uso típicos
+
+### 9.1 Primeiro uso (cauteloso)
+
+```bash
+# 1. Setup com UV (recomendado)
+uv venv
+source .venv/bin/activate
+uv pip install -e ".[dev]"
+cp .env.example .env # editar com API_ID e API_HASH
+
+# 2. Testar com dry-run
+python -m clean_telegram --dry-run
+
+# 3. Testar com poucos diálogos
+python -m clean_telegram --dry-run --limit 5
+
+# 4. Executar de verdade (poucos diálogos)
+python -m clean_telegram --limit 5
+# → Digita "APAGAR TUDO"
+
+# 5. Executar em tudo
+python -m clean_telegram
+# → Digita "APAGAR TUDO"
+```
+
+### 9.2 Uso automatizado (script/cron)
+
+```bash
+python -m clean_telegram --yes
+# Pula confirmação interativa — usar com cuidado!
+```
+
+### 9.3 Debugging de rate limit
+
+```bash
+python -m clean_telegram --limit 3
+# Observar logs de FloodWaitError
+# Ajustar --limit conforme necessário
+```
+
+### 9.4 Executar testes
+
+```bash
+# Executar todos os testes
+pytest
+
+# Executar com cobertura
+pytest --cov=src/clean_telegram --cov-report=html
+
+# Executar teste específico
+pytest tests/test_client.py -v
+```
+
+---
+
+## 10. Diagrama de ciclo de vida da sessão
+
+```text
+ Primeira execução Execuções seguintes
+ ────────────────── ───────────────────
+ │ │
+ ▼ ▼
+ ┌──────────────┐ ┌──────────────┐
+ │ Sem sessão │ │ session. │
+ │ local │ │ session │
+ └──────┬───────┘ │ existe │
+ │ └──────┬───────┘
+ ▼ ▼
+ ┌──────────────┐ ┌──────────────┐
+ │ Telefone │ │ Login │
+ │ + Código │ │ automático │
+ │ + (2FA) │ │ │
+ └──────┬───────┘ └──────┬───────┘
+ │ │
+ ▼ ▼
+ ┌──────────────┐ ┌──────────────┐
+ │ Cria │ │ Reutiliza │
+ │ session. │ │ sessão │
+ │ session │ │ existente │
+ └──────┬───────┘ └──────┬───────┘
+ │ │
+ └──────────┬───────────────────┘
+ ▼
+ ┌──────────────┐
+ │ Executa │
+ │ limpeza │
+ └──────────────┘
+```
+
+> **Importante:** O arquivo `*.session` contém credenciais de autenticação e **nunca** deve ser commitado no repositório. Ele já está configurado no `.gitignore`.
+
+---
+
+## 11. Comandos de Desenvolvimento
+
+```bash
+# Verificar ajuda
+python -m clean_telegram --help
+
+# Formatar código
+black src/ tests/
+
+# Ordenar imports
+isort src/ tests/
+
+# Executar linting
+flake8 src/ tests/
+
+# Verificar tipos
+mypy src/
+
+# Executar todos os checks de uma vez
+black --check src/ tests/ && isort --check-only src/ tests/ && flake8 src/ tests/ && mypy src/
+```
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..593ba81
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,144 @@
+[build-system]
+requires = ["setuptools>=68.0", "wheel"]
+build-backend = "setuptools.build_meta"
+
+[project]
+name = "clean-telegram"
+version = "0.1.0"
+description = "Script para limpar conta Telegram via Telethon"
+readme = "README.md"
+requires-python = ">=3.10"
+license = {text = "MIT"}
+authors = [
+ {name = "Gabriel Ramos"}
+]
+keywords = ["telegram", "telethon", "cleanup", "automation", "privacy"]
+classifiers = [
+ "Development Status :: 3 - Alpha",
+ "Intended Audience :: End Users/Desktop",
+ "License :: OSI Approved :: MIT License",
+ "Programming Language :: Python :: 3",
+ "Programming Language :: Python :: 3.10",
+ "Programming Language :: Python :: 3.11",
+ "Programming Language :: Python :: 3.12",
+ "Topic :: Utilities",
+]
+
+dependencies = [
+ "telethon>=1.42.0,<2.0.0",
+ "python-dotenv>=1.0.0,<2.0.0",
+ "qrcode>=8.0,<9.0",
+]
+
+[project.optional-dependencies]
+dev = [
+ "black>=24.0.0",
+ "isort>=5.13.0",
+ "flake8>=7.0.0",
+ "mypy>=1.8.0",
+ "pytest>=8.0.0",
+ "pytest-asyncio>=0.23.0",
+ "pytest-cov>=4.1.0",
+]
+
+[project.scripts]
+clean-telegram = "clean_telegram.__main__:main"
+
+[project.urls]
+Homepage = "https://github.com/gabrielramos/CleanTelegram"
+Repository = "https://github.com/gabrielramos/CleanTelegram"
+
+[tool.setuptools.packages.find]
+where = ["src"]
+
+[tool.setuptools.package-data]
+clean_telegram = ["py.typed"]
+
+# Black configuration
+[tool.black]
+line-length = 100
+target-version = ["py310", "py311", "py312"]
+include = '\.pyi?$'
+exclude = '''
+/(
+ \.git
+ | \.venv
+ | \.eggs
+ | \.tox
+ | build
+ | dist
+ | __pycache__
+)/
+'''
+
+# isort configuration
+[tool.isort]
+profile = "black"
+line_length = 100
+src_paths = ["src", "tests"]
+skip_gitignore = true
+
+# MyPy configuration
+[tool.mypy]
+python_version = "3.10"
+warn_return_any = true
+warn_unused_configs = true
+disallow_untyped_defs = false
+disallow_incomplete_defs = false
+check_untyped_defs = true
+ignore_missing_imports = true
+warn_redundant_casts = true
+warn_unused_ignores = true
+warn_no_return = true
+strict_equality = true
+show_error_codes = true
+plugins = []
+
+[[tool.mypy.overrides]]
+module = "telethon.*"
+ignore_missing_imports = true
+
+# Pytest configuration
+[tool.pytest.ini_options]
+testpaths = ["tests"]
+python_files = ["test_*.py", "*_test.py"]
+python_classes = ["Test*"]
+python_functions = ["test_*"]
+addopts = [
+ "--strict-markers",
+ "--strict-config",
+ "--cov=src/clean_telegram",
+ "--cov-report=term-missing",
+ "--cov-report=html",
+ "-v",
+]
+asyncio_mode = "auto"
+markers = [
+ "asyncio: mark test as async",
+ "integration: mark test as integration test",
+]
+
+# Coverage configuration
+[tool.coverage.run]
+source = ["src"]
+branch = true
+parallel = true
+
+[tool.coverage.paths]
+source = [
+ "src/",
+]
+
+[tool.coverage.report]
+fail_under = 55
+show_missing = true
+skip_covered = false
+exclude_lines = [
+ "pragma: no cover",
+ "def __repr__",
+ "raise AssertionError",
+ "raise NotImplementedError",
+ "if __name__ == .__main__.:",
+ "if TYPE_CHECKING:",
+ "if typing.TYPE_CHECKING:",
+]
diff --git a/requirements-dev.txt b/requirements-dev.txt
new file mode 100644
index 0000000..297346f
--- /dev/null
+++ b/requirements-dev.txt
@@ -0,0 +1,16 @@
+# Dependências de desenvolvimento
+-r requirements.txt
+
+# Código quality
+black>=24.0.0
+isort>=5.13.0
+flake8>=7.0.0
+mypy>=1.8.0
+
+# Testes
+pytest>=8.0.0
+pytest-asyncio>=0.23.0
+pytest-cov>=4.1.0
+
+# Types
+types-python-dotenv>=1.0.0
diff --git a/requirements.txt b/requirements.txt
index 4a2fccd..959ca72 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,2 +1,4 @@
-telethon==1.42.0
-python-dotenv==1.2.1
+# Dependências de produção
+telethon>=1.42.0,<2.0.0
+python-dotenv>=1.0.0,<2.0.0
+qrcode>=8.0,<9.0
diff --git a/src/clean_telegram/__init__.py b/src/clean_telegram/__init__.py
new file mode 100644
index 0000000..25b2f0e
--- /dev/null
+++ b/src/clean_telegram/__init__.py
@@ -0,0 +1,14 @@
+"""CleanTelegram: script para limpar conta Telegram via Telethon.
+
+Este pacote fornece funcionalidades para:
+- Apagar históricos de conversa (usuários/bots)
+- Sair de grupos/canais
+- Gerenciar diálogos do Telegram de forma programática
+"""
+
+__version__ = "0.1.0"
+
+from .client import process_dialog
+from .utils import env_int, safe_sleep
+
+__all__ = ["process_dialog", "env_int", "safe_sleep"]
diff --git a/src/clean_telegram/__main__.py b/src/clean_telegram/__main__.py
new file mode 100644
index 0000000..8fa1799
--- /dev/null
+++ b/src/clean_telegram/__main__.py
@@ -0,0 +1,210 @@
+"""Entry-point para execução do CleanTelegram."""
+
+import argparse
+import asyncio
+import logging
+import os
+import sys
+
+import qrcode
+from dotenv import load_dotenv
+from telethon import TelegramClient
+from telethon.errors import FloodWaitError, RPCError, SessionPasswordNeededError
+
+from clean_telegram.client import process_dialog
+from clean_telegram.utils import env_int, resolve_session_name, safe_sleep
+
+logger = logging.getLogger(__name__)
+
+
+def display_qr_code(url: str) -> None:
+ """Exibe o QR code no terminal usando ASCII.
+
+ Args:
+ url: URL para codificar no QR code.
+ """
+ qr = qrcode.QRCode(
+ version=1,
+ error_correction=qrcode.constants.ERROR_CORRECT_L,
+ box_size=1,
+ border=2,
+ )
+ qr.add_data(url)
+ qr.print_ascii(invert=True)
+ print(f"\nURL: {url}\n")
+
+
+async def login_with_qr(client: TelegramClient) -> bool:
+ """Realiza login usando QR code.
+
+ Args:
+ client: Instância do TelegramClient.
+
+ Returns:
+ True se login foi bem-sucedido, False caso contrário.
+ """
+ logger.info("Iniciando login via QR code...")
+
+ qr_login = await client.qr_login()
+
+ print("\nEscaneie o QR code abaixo com o seu Telegram mobile:")
+ print("(Telegram > Configurações > Dispositivos > Escanear QR Code)\n")
+
+ while not qr_login.is_logged:
+ display_qr_code(qr_login.url)
+
+ try:
+ logger.info("Aguardando leitura do QR code...")
+ await asyncio.wait_for(qr_login.wait(), timeout=10)
+
+ except asyncio.TimeoutError:
+ # Timeout é esperado - o QR code expira e precisamos gerar outro
+ logger.info("QR code expirado, gerando novo...")
+ continue
+
+ except SessionPasswordNeededError:
+ print("\nVerificação em duas etapas (2FA) habilitada.")
+ print("Por favor, use o login por telefone/código.")
+ return False
+
+ except Exception as e:
+ logger.error("Erro durante login via QR code: %s", e)
+ return False
+
+ logger.info("Login via QR code realizado com sucesso!")
+ return True
+
+
+async def main() -> None:
+ """Entry-point assíncrono."""
+ logging.basicConfig(
+ level=logging.INFO,
+ format="%(asctime)s %(levelname)s %(name)s: %(message)s",
+ )
+
+ load_dotenv()
+
+ parser = argparse.ArgumentParser(
+ description="Apaga diálogos e sai de grupos/canais (Telethon)."
+ )
+ parser.add_argument(
+ "--dry-run",
+ action="store_true",
+ help="Não faz alterações; só imprime o que faria.",
+ )
+ parser.add_argument(
+ "--yes",
+ action="store_true",
+ help="Não pedir confirmação interativa.",
+ )
+ parser.add_argument(
+ "--limit",
+ type=int,
+ default=0,
+ help="Limita quantos diálogos processar (0 = todos).",
+ )
+ parser.add_argument(
+ "--qr-login",
+ action="store_true",
+ help="Usa login via QR code em vez de telefone/código.",
+ )
+ args = parser.parse_args()
+
+ try:
+ api_id = env_int("API_ID")
+ except SystemExit:
+ raise SystemExit("Faltou API_ID no .env ou valor inválido")
+
+ api_hash = os.getenv("API_HASH")
+ if not api_hash:
+ raise SystemExit("Faltou API_HASH no .env")
+
+ session_name = resolve_session_name(os.getenv("SESSION_NAME", "session"))
+
+ # Se for login via QR code, usa uma sessão temporária
+ if args.qr_login:
+ # Remove extensão .session se existir para criar sessão temporária
+ base_name = session_name.replace(".session", "")
+ session_name = f"{base_name}_qr.session"
+
+ if not args.dry_run and not args.yes:
+ print(
+ "ATENÇÃO: isso vai apagar conversas e sair de grupos/canais.\n"
+ "Digite 'APAGAR TUDO' para confirmar: ",
+ end="",
+ flush=True,
+ )
+ confirm = sys.stdin.readline().strip()
+ if confirm != "APAGAR TUDO":
+ print("Cancelado.")
+ return
+
+ async with TelegramClient(session_name, api_id, api_hash) as client:
+ # Login via QR code se solicitado
+ if args.qr_login:
+ logger.info("Modo de login via QR code ativado.")
+ # Conecta o cliente antes de tentar QR login
+ await client.connect()
+ # Tenta fazer login via QR code
+ success = await login_with_qr(client)
+ if not success:
+ logger.error("Falha no login via QR code. Encerrando.")
+ return
+
+ me = await client.get_me()
+ logger.info("Logado como: %s (id=%s)", me.username or me.first_name, me.id)
+
+ processed = 0
+ async for d in client.iter_dialogs():
+ if args.limit and processed >= args.limit:
+ break
+
+ title = d.name or "(sem nome)"
+ entity = d.entity
+ index = processed + 1
+
+ # FloodWait retry (não pular o diálogo)
+ max_retries = 5
+ attempt = 0
+ while True:
+ try:
+ await process_dialog(
+ client,
+ entity,
+ title,
+ index,
+ dry_run=args.dry_run,
+ )
+ await safe_sleep(0.35)
+ # Só incrementa se processou com sucesso
+ processed += 1
+ break
+
+ except FloodWaitError as e:
+ attempt += 1
+ wait_s = int(e.seconds)
+ logger.warning(
+ "Rate limit (FloodWait) em '%s'. Aguardando %ss (tentativa %s/%s)...",
+ title,
+ wait_s,
+ attempt,
+ max_retries,
+ )
+ await asyncio.sleep(wait_s)
+ if attempt >= max_retries:
+ logger.error("Max retries atingido; pulando '%s'.", title)
+ break
+
+ except RPCError:
+ logger.exception("RPCError em '%s'", title)
+ break
+
+ except Exception:
+ logger.exception("Erro inesperado em '%s'", title)
+ break
+
+ logger.info("Concluído. Diálogos processados: %s", processed)
+
+
+if __name__ == "__main__":
+ asyncio.run(main())
diff --git a/src/clean_telegram/client.py b/src/clean_telegram/client.py
new file mode 100644
index 0000000..064a163
--- /dev/null
+++ b/src/clean_telegram/client.py
@@ -0,0 +1,124 @@
+"""Funções para interação com cliente Telegram."""
+
+import logging
+from typing import Union
+
+from telethon import TelegramClient
+from telethon.errors import FloodWaitError, RPCError
+from telethon.tl.functions.channels import LeaveChannelRequest
+from telethon.tl.functions.messages import DeleteChatUserRequest, DeleteHistoryRequest
+from telethon.tl.types import Channel, Chat, InputUserSelf, User
+
+logger = logging.getLogger(__name__)
+
+
+async def delete_dialog(
+ client: TelegramClient, peer: Union[Channel, Chat, User, InputUserSelf], *, dry_run: bool
+) -> None:
+ """Apaga o histórico do diálogo (tenta revogar quando aplicável).
+
+ Args:
+ client: Instância do TelegramClient.
+ peer: Entidade do diálogo.
+ dry_run: Se True, não executa ações destrutivas.
+ """
+ if dry_run:
+ return
+ await client(DeleteHistoryRequest(peer=peer, max_id=0, just_clear=False, revoke=True))
+
+
+async def leave_channel(client: TelegramClient, entity: Channel, *, dry_run: bool) -> None:
+ """Sai de um canal/megagrupo (Channel).
+
+ Args:
+ client: Instância do TelegramClient.
+ entity: Entidade do canal.
+ dry_run: Se True, não executa ações destrutivas.
+ """
+ if dry_run:
+ return
+ await client(LeaveChannelRequest(entity))
+
+
+async def leave_legacy_chat(client: TelegramClient, entity: Chat, *, dry_run: bool) -> None:
+ """Sai de um grupo antigo (Chat).
+
+ Telethon/Telegram têm diferenças entre Chat (grupo antigo) e Channel (canal/megagrupo).
+
+ Args:
+ client: Instância do TelegramClient.
+ entity: Entidade do chat.
+ dry_run: Se True, não executa ações destrutivas.
+ """
+ if dry_run:
+ return
+
+ # Remove o próprio usuário do chat legado.
+ await client(DeleteChatUserRequest(chat_id=entity.id, user_id=InputUserSelf()))
+
+
+async def process_dialog(
+ client: TelegramClient,
+ entity: Union[Channel, Chat, User],
+ title: str,
+ index: int,
+ *,
+ dry_run: bool,
+) -> bool:
+ """Processa um único diálogo, escolhendo a ação correta por tipo.
+
+ Args:
+ client: Instância do TelegramClient.
+ entity: Entidade do diálogo.
+ title: Nome do diálogo.
+ index: Índice do diálogo para log.
+ dry_run: Se True, não executa ações destrutivas.
+
+ Returns:
+ True se processado com sucesso, False se ocorreu erro.
+ """
+ try:
+ if isinstance(entity, Channel):
+ logger.info("[%s] SAIR de canal/megagrupo: %s", index, title)
+ await leave_channel(client, entity, dry_run=dry_run)
+ return True
+
+ if isinstance(entity, Chat):
+ logger.info("[%s] SAIR de grupo legado (Chat): %s", index, title)
+ try:
+ await leave_legacy_chat(client, entity, dry_run=dry_run)
+ except RPCError:
+ logger.warning(
+ "Falha ao sair via DeleteChatUserRequest; tentando fallback delete_dialog: %s",
+ title,
+ )
+ if not dry_run:
+ await client.delete_dialog(entity)
+ return True
+
+ if isinstance(entity, User):
+ logger.info("[%s] APAGAR conversa: %s", index, title)
+ await delete_dialog(client, entity, dry_run=dry_run)
+ return True
+
+ logger.info("[%s] APAGAR diálogo (tipo desconhecido): %s", index, title)
+ if not dry_run:
+ await client.delete_dialog(entity)
+ return True
+
+ except FloodWaitError as e:
+ logger.error(
+ "Erro ao processar diálogo '%s': FloodWaitError (aguardar %ss): %s",
+ title,
+ e.seconds,
+ e,
+ )
+ return False
+
+ except RPCError as e:
+ logger.error("Erro ao processar diálogo '%s': %s", title, e)
+ return False
+
+ except Exception:
+ logger.exception("Erro inesperado ao processar diálogo '%s'", title)
+ return False
diff --git a/src/clean_telegram/utils.py b/src/clean_telegram/utils.py
new file mode 100644
index 0000000..eac818e
--- /dev/null
+++ b/src/clean_telegram/utils.py
@@ -0,0 +1,101 @@
+"""Funções utilitárias para CleanTelegram."""
+
+import asyncio
+import logging
+import os
+import shutil
+from pathlib import Path
+
+logger = logging.getLogger(__name__)
+
+
+def env_int(name: str) -> int:
+ """Lê uma variável de ambiente obrigatória e converte para int.
+
+ Args:
+ name: Nome da variável de ambiente.
+
+ Returns:
+ Valor da variável convertido para int.
+
+ Raises:
+ SystemExit: Se a variável não estiver definida ou for inválida.
+ """
+ v = os.getenv(name)
+ if not v:
+ logger.error("Variável de ambiente %s não definida", name)
+ raise SystemExit(f"Faltou {name} no .env")
+ if v.strip() != v:
+ logger.warning("Variável de ambiente %s contém espaços em branco", name)
+ v = v.strip()
+ try:
+ return int(v)
+ except ValueError:
+ logger.error("Variável de ambiente %s contém valor inválido: %s", name, v)
+ raise SystemExit(f"{name} deve ser um número inteiro válido")
+
+
+async def safe_sleep(seconds: float) -> None:
+ """Sleep curto para reduzir risco de rate limit.
+
+ Args:
+ seconds: Tempo de espera em segundos. Deve ser um número não negativo.
+
+ Raises:
+ ValueError: Se seconds não for um número ou for negativo.
+ """
+ if not isinstance(seconds, (int, float)):
+ raise ValueError("safe_sleep: seconds deve ser um número (int ou float)")
+ if seconds < 0:
+ raise ValueError("safe_sleep: seconds deve ser não negativo")
+
+ if seconds > 0:
+ logger.debug("Aguardando %.2fs antes da próxima operação", seconds)
+ await asyncio.sleep(seconds)
+
+
+def _session_db_path(base_dir: Path, session_name: str) -> Path:
+ """Retorna o caminho esperado do arquivo SQLite da sessão."""
+ filename = session_name if session_name.endswith(".session") else f"{session_name}.session"
+ return base_dir / filename
+
+
+def resolve_session_name(
+ session_name: str | None, *, cwd: Path | None = None, home: Path | None = None
+) -> str:
+ """Resolve o nome/caminho de sessão para evitar novo login frequente.
+
+ Regras:
+ - Se `SESSION_NAME` for caminho absoluto, usa esse caminho.
+ - Se `SESSION_NAME` tiver diretório relativo (ex.: `data/minha_sessao`),
+ resolve relativo ao diretório atual.
+ - Se for apenas nome simples (ex.: `session`), usa por padrão
+ `~/.clean_telegram/` para manter a sessão estável entre execuções
+ em diretórios diferentes.
+ - Se existir sessão legada no diretório atual e não existir no novo local,
+ migra automaticamente.
+ """
+ current_dir = cwd or Path.cwd()
+ home_dir = home or Path.home()
+
+ name = (session_name or "session").strip() or "session"
+ path_candidate = Path(name).expanduser()
+ has_directory_hint = "/" in name or "\\" in name
+
+ if path_candidate.is_absolute() or has_directory_hint:
+ resolved = (
+ path_candidate if path_candidate.is_absolute() else (current_dir / path_candidate)
+ )
+ resolved.parent.mkdir(parents=True, exist_ok=True)
+ return str(resolved)
+
+ session_dir = home_dir / ".clean_telegram"
+ session_dir.mkdir(parents=True, exist_ok=True)
+ modern_session = _session_db_path(session_dir, name)
+ legacy_session = _session_db_path(current_dir, name)
+
+ if legacy_session.exists() and not modern_session.exists():
+ shutil.copy2(legacy_session, modern_session)
+ logger.info("Sessão migrada para %s", modern_session)
+
+ return str(session_dir / name)
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 0000000..5083776
--- /dev/null
+++ b/tests/__init__.py
@@ -0,0 +1 @@
+"""Testes para CleanTelegram."""
diff --git a/tests/conftest.py b/tests/conftest.py
new file mode 100644
index 0000000..bd029ce
--- /dev/null
+++ b/tests/conftest.py
@@ -0,0 +1,150 @@
+"""Configuração e fixtures para testes do CleanTelegram."""
+
+import os
+from contextlib import asynccontextmanager
+from unittest.mock import AsyncMock, MagicMock, Mock
+
+import pytest
+from telethon.tl.types import Channel, Chat, Dialog, User
+
+
+@pytest.fixture
+def mock_telegram_client():
+ """Fixture que retorna um mock do TelegramClient."""
+ client = AsyncMock()
+ client.get_me = AsyncMock(return_value=Mock(username="testuser", id=12345, first_name="Test"))
+ client.iter_dialogs = AsyncMock()
+ client.delete_dialog = AsyncMock()
+ return client
+
+
+@pytest.fixture
+def mock_async_telegram_client():
+ """Fixture que retorna um AsyncMock do TelegramClient como context manager.
+
+ Este fixture simula o comportamento do TelegramClient quando usado com
+ 'async with', retornando um client que já implementa os métodos necessários.
+ """
+ client = AsyncMock()
+ client.get_me = AsyncMock(return_value=Mock(username="testuser", id=12345, first_name="Test"))
+ client.iter_dialogs = AsyncMock()
+ client.delete_dialog = AsyncMock()
+ client.return_value = client
+
+ @asynccontextmanager
+ async def _client_context(*args, **kwargs):
+ yield client
+
+ mock_client = MagicMock()
+ mock_client.__aenter__ = AsyncMock(return_value=client)
+ mock_client.__aexit__ = AsyncMock(return_value=None)
+
+ return mock_client
+
+
+@pytest.fixture
+def monkeypatch_env(temp_env_vars):
+ """Fixture para monkeypatch variáveis de ambiente.
+
+ Usa temp_env_vars como base e retorna uma função auxiliar para setar vars.
+ """
+
+ def set_env(**kwargs):
+ for key, value in kwargs.items():
+ if value is None:
+ os.environ.pop(key, None)
+ else:
+ os.environ[key] = str(value)
+
+ return set_env
+
+
+@pytest.fixture
+def mock_channel():
+ """Fixture que retorna um mock de Channel (canal/megagrupo)."""
+ channel = MagicMock(spec=Channel)
+ channel.id = 123456
+ channel.title = "Test Channel"
+ channel.username = "testchannel"
+ return channel
+
+
+@pytest.fixture
+def mock_chat():
+ """Fixture que retorna um mock de Chat (grupo legado)."""
+ chat = MagicMock(spec=Chat)
+ chat.id = 789012
+ chat.title = "Test Chat"
+ return chat
+
+
+@pytest.fixture
+def mock_user():
+ """Fixture que retorna um mock de User (usuário/bot)."""
+ user = MagicMock(spec=User)
+ user.id = 345678
+ user.first_name = "Test"
+ user.last_name = "User"
+ user.username = "testuser"
+ user.bot = False
+ return user
+
+
+@pytest.fixture
+def mock_bot():
+ """Fixture que retorna um mock de bot."""
+ bot = MagicMock(spec=User)
+ bot.id = 456789
+ bot.first_name = "TestBot"
+ bot.last_name = "" # Bots geralmente não têm last_name
+ bot.username = "testbot"
+ bot.bot = True
+ return bot
+
+
+@pytest.fixture
+def mock_dialog():
+ """Fixture que retorna um mock de Dialog."""
+ dialog = MagicMock(spec=Dialog)
+ dialog.name = "Test Dialog"
+ dialog.entity = MagicMock()
+ return dialog
+
+
+@pytest.fixture
+def temp_env_vars():
+ """Fixture que limpa e restaura variáveis de ambiente."""
+ original_env = os.environ.copy()
+ yield
+ os.environ.clear()
+ os.environ.update(original_env)
+
+
+@pytest.fixture
+def mock_session_file(tmp_path):
+ """Fixture que cria um arquivo de sessão temporário."""
+ session_file = tmp_path / "test.session"
+ session_file.touch()
+ return session_file
+
+
+@pytest.fixture
+def mock_dialog_factory():
+ """Factory function para criar mocks de Dialog.
+
+ Retorna uma função que pode criar diálogos com parâmetros customizáveis.
+ """
+
+ def _create_dialog(
+ name: str = "Test Dialog",
+ entity=None,
+ dialog_id: int = 12345,
+ ):
+ """Cria um mock de Dialog com parâmetros customizáveis."""
+ dialog = MagicMock(spec=Dialog)
+ dialog.name = name
+ dialog.id = dialog_id
+ dialog.entity = entity or MagicMock()
+ return dialog
+
+ return _create_dialog
diff --git a/tests/test_client.py b/tests/test_client.py
new file mode 100644
index 0000000..f3543d4
--- /dev/null
+++ b/tests/test_client.py
@@ -0,0 +1,243 @@
+"""Testes para o módulo client.py."""
+
+from unittest.mock import AsyncMock, MagicMock
+
+import pytest
+from telethon.errors import BadRequestError, FloodWaitError
+from telethon.tl.functions.channels import LeaveChannelRequest
+from telethon.tl.functions.messages import DeleteChatUserRequest, DeleteHistoryRequest
+
+from clean_telegram.client import (
+ delete_dialog,
+ leave_channel,
+ leave_legacy_chat,
+ process_dialog,
+)
+
+
+class TestDeleteDialog:
+ """Testes para delete_dialog."""
+
+ @pytest.mark.asyncio
+ async def test_delete_dialog_dry_run(self, mock_telegram_client):
+ """Testa que delete_dialog não executa em dry_run."""
+ peer = MagicMock()
+
+ await delete_dialog(mock_telegram_client, peer, dry_run=True)
+
+ mock_telegram_client.assert_not_awaited()
+
+ @pytest.mark.asyncio
+ async def test_delete_dialog_executes(self, mock_telegram_client):
+ """Testa que delete_dialog executa DeleteHistoryRequest."""
+ peer = MagicMock()
+ mock_telegram_client.return_value = None
+
+ await delete_dialog(mock_telegram_client, peer, dry_run=False)
+
+ mock_telegram_client.assert_called_once()
+ call_args = mock_telegram_client.call_args
+ assert isinstance(call_args[0][0], DeleteHistoryRequest)
+
+
+class TestLeaveChannel:
+ """Testes para leave_channel."""
+
+ @pytest.mark.asyncio
+ async def test_leave_channel_dry_run(self, mock_telegram_client, mock_channel):
+ """Testa que leave_channel não executa em dry_run."""
+ await leave_channel(mock_telegram_client, mock_channel, dry_run=True)
+
+ mock_telegram_client.assert_not_awaited()
+
+ @pytest.mark.asyncio
+ async def test_leave_channel_executes(self, mock_telegram_client, mock_channel):
+ """Testa que leave_channel executa LeaveChannelRequest."""
+ mock_telegram_client.return_value = None
+
+ await leave_channel(mock_telegram_client, mock_channel, dry_run=False)
+
+ mock_telegram_client.assert_called_once()
+ call_args = mock_telegram_client.call_args
+ assert isinstance(call_args[0][0], LeaveChannelRequest)
+
+
+class TestLeaveLegacyChat:
+ """Testes para leave_legacy_chat."""
+
+ @pytest.mark.asyncio
+ async def test_leave_legacy_chat_dry_run(self, mock_telegram_client, mock_chat):
+ """Testa que leave_legacy_chat não executa em dry_run."""
+ await leave_legacy_chat(mock_telegram_client, mock_chat, dry_run=True)
+
+ mock_telegram_client.assert_not_awaited()
+
+ @pytest.mark.asyncio
+ async def test_leave_legacy_chat_executes(self, mock_telegram_client, mock_chat):
+ """Testa que leave_legacy_chat executa DeleteChatUserRequest."""
+ mock_telegram_client.return_value = None
+
+ await leave_legacy_chat(mock_telegram_client, mock_chat, dry_run=False)
+
+ mock_telegram_client.assert_called_once()
+ call_args = mock_telegram_client.call_args
+ assert isinstance(call_args[0][0], DeleteChatUserRequest)
+
+
+class TestProcessDialog:
+ """Testes para process_dialog."""
+
+ @pytest.mark.asyncio
+ async def test_process_channel(self, mock_telegram_client, mock_channel):
+ """Testa process_dialog com Channel."""
+ mock_telegram_client.return_value = None
+
+ result = await process_dialog(
+ mock_telegram_client,
+ mock_channel,
+ "Test Channel",
+ 1,
+ dry_run=False,
+ )
+
+ assert result is True
+
+ @pytest.mark.asyncio
+ async def test_process_channel_dry_run(self, mock_telegram_client, mock_channel):
+ """Testa process_dialog com Channel em dry_run."""
+ result = await process_dialog(
+ mock_telegram_client,
+ mock_channel,
+ "Test Channel",
+ 1,
+ dry_run=True,
+ )
+
+ assert result is True
+ mock_telegram_client.assert_not_awaited()
+
+ @pytest.mark.asyncio
+ async def test_process_chat(self, mock_telegram_client, mock_chat):
+ """Testa process_dialog com Chat."""
+ mock_telegram_client.return_value = None
+
+ result = await process_dialog(
+ mock_telegram_client,
+ mock_chat,
+ "Test Chat",
+ 1,
+ dry_run=False,
+ )
+
+ assert result is True
+
+ @pytest.mark.asyncio
+ async def test_process_chat_fallback_on_rpc_error(self, mock_telegram_client, mock_chat):
+ """Testa fallback em process_dialog quando RPCError ocorre em Chat."""
+
+ # Configura side_effect para a primeira chamada (DeleteChatUserRequest) levantar erro
+ # e mock delete_dialog para o fallback
+ mock_telegram_client.side_effect = BadRequestError(None, "Test error")
+ mock_telegram_client.delete_dialog = AsyncMock()
+
+ result = await process_dialog(
+ mock_telegram_client,
+ mock_chat,
+ "Test Chat",
+ 1,
+ dry_run=False,
+ )
+
+ assert result is True
+ mock_telegram_client.delete_dialog.assert_called_once_with(mock_chat)
+
+ @pytest.mark.asyncio
+ async def test_process_user(self, mock_telegram_client, mock_user):
+ """Testa process_dialog com User."""
+ mock_telegram_client.return_value = None
+
+ result = await process_dialog(
+ mock_telegram_client,
+ mock_user,
+ "Test User",
+ 1,
+ dry_run=False,
+ )
+
+ assert result is True
+
+ @pytest.mark.asyncio
+ async def test_process_bot(self, mock_telegram_client, mock_bot):
+ """Testa process_dialog com bot."""
+ mock_telegram_client.return_value = None
+
+ result = await process_dialog(
+ mock_telegram_client,
+ mock_bot,
+ "TestBot",
+ 1,
+ dry_run=False,
+ )
+
+ assert result is True
+
+ @pytest.mark.asyncio
+ async def test_process_unknown_type(self, mock_telegram_client):
+ """Testa process_dialog com tipo desconhecido."""
+ unknown_entity = MagicMock(spec=object)
+ mock_telegram_client.return_value = None
+
+ result = await process_dialog(
+ mock_telegram_client,
+ unknown_entity,
+ "Unknown",
+ 1,
+ dry_run=False,
+ )
+
+ assert result is True
+
+ @pytest.mark.asyncio
+ async def test_process_dialog_flood_wait_error(self, mock_telegram_client, mock_channel):
+ """Testa process_dialog com FloodWaitError."""
+ mock_telegram_client.side_effect = FloodWaitError(None, 60)
+
+ result = await process_dialog(
+ mock_telegram_client,
+ mock_channel,
+ "Test Channel",
+ 1,
+ dry_run=False,
+ )
+
+ assert result is False
+
+ @pytest.mark.asyncio
+ async def test_process_dialog_rpc_error(self, mock_telegram_client, mock_channel):
+ """Testa process_dialog com RPCError."""
+ mock_telegram_client.side_effect = BadRequestError(None, "Test error")
+
+ result = await process_dialog(
+ mock_telegram_client,
+ mock_channel,
+ "Test Channel",
+ 1,
+ dry_run=False,
+ )
+
+ assert result is False
+
+ @pytest.mark.asyncio
+ async def test_process_dialog_generic_exception(self, mock_telegram_client, mock_channel):
+ """Testa process_dialog com exceção genérica."""
+ mock_telegram_client.side_effect = Exception("Unexpected error")
+
+ result = await process_dialog(
+ mock_telegram_client,
+ mock_channel,
+ "Test Channel",
+ 1,
+ dry_run=False,
+ )
+
+ assert result is False
diff --git a/tests/test_main.py b/tests/test_main.py
new file mode 100644
index 0000000..c660d8f
--- /dev/null
+++ b/tests/test_main.py
@@ -0,0 +1,562 @@
+"""Testes para o módulo __main__.py."""
+
+import asyncio
+from contextlib import asynccontextmanager
+from unittest.mock import AsyncMock, MagicMock, Mock, patch
+
+import pytest
+from telethon.errors import FloodWaitError, RPCError
+from telethon.tl.types import Channel, User
+
+# Importamos aqui para evitar problemas de importação
+from clean_telegram import __main__
+
+
+@pytest.fixture
+def mock_telegram_client_context():
+ """Fixture que cria um mock do TelegramClient como context manager.
+
+ Retorna uma função que pode ser usada para criar o mock com diferentes
+ configurações de iter_dialogs.
+ """
+
+ def _create_mock(iter_dialogs_return=None):
+ """Cria um mock do TelegramClient."""
+ # Criar async generator function baseado nos items
+ if callable(iter_dialogs_return):
+ # Se passou uma função, usar ela diretamente
+ async def iter_dialogs_func():
+ async for item in iter_dialogs_return():
+ yield item
+
+ elif iter_dialogs_return is None:
+ # Lista vazia
+ async def iter_dialogs_func():
+ return
+ yield
+
+ else:
+ # Lista de items
+ items = iter_dialogs_return
+
+ async def iter_dialogs_func():
+ for item in items:
+ yield item
+
+ client = AsyncMock()
+ client.get_me = AsyncMock(
+ return_value=Mock(username="testuser", id=12345, first_name="Test")
+ )
+ client.iter_dialogs = iter_dialogs_func
+
+ @asynccontextmanager
+ async def _client_context(*args, **kwargs):
+ yield client
+
+ mock_client = MagicMock()
+ mock_client.__aenter__ = AsyncMock(return_value=client)
+ mock_client.__aexit__ = AsyncMock(return_value=None)
+
+ return mock_client
+
+ return _create_mock
+
+
+class TestMainHelpFlag:
+ """Testes para flag --help."""
+
+ def test_main_help_flag(self, capsys):
+ """Testa exibição de ajuda com --help."""
+ with patch("sys.argv", ["clean-telegram", "--help"]):
+ with pytest.raises(SystemExit) as exc_info:
+ asyncio.run(__main__.main())
+
+ # --help causa SystemExit com código 0
+ assert exc_info.value.code == 0
+
+
+class TestMainEnvValidation:
+ """Testes para validação de variáveis de ambiente."""
+
+ @pytest.mark.asyncio
+ async def test_main_missing_api_id(self, monkeypatch_env, mock_telegram_client_context):
+ """Testa erro quando API_ID não está definido."""
+ monkeypatch_env(API_ID=None, API_HASH="test_hash")
+
+ # Mock TelegramClient no módulo __main__ para evitar tentativa de conexão real
+ with patch(
+ "clean_telegram.__main__.TelegramClient", return_value=mock_telegram_client_context()
+ ):
+ # Mock load_dotenv para não carregar .env real
+ with patch("clean_telegram.__main__.load_dotenv"):
+ # Usar --dry-run para pular confirmação e chegar na validação de API_ID
+ with patch("sys.argv", ["clean-telegram", "--dry-run"]):
+ with pytest.raises(SystemExit) as exc_info:
+ await __main__.main()
+
+ assert "API_ID" in str(exc_info.value)
+
+ @pytest.mark.asyncio
+ async def test_main_missing_api_hash(self, monkeypatch_env, mock_telegram_client_context):
+ """Testa erro quando API_HASH não está definido."""
+ monkeypatch_env(API_ID="12345", API_HASH=None)
+
+ # Mock TelegramClient no módulo __main__ para evitar tentativa de conexão real
+ with patch(
+ "clean_telegram.__main__.TelegramClient", return_value=mock_telegram_client_context()
+ ):
+ # Mock load_dotenv para não carregar .env real
+ with patch("clean_telegram.__main__.load_dotenv"):
+ # Usar --dry-run para pular confirmação e chegar na validação de API_HASH
+ with patch("sys.argv", ["clean-telegram", "--dry-run"]):
+ with pytest.raises(SystemExit) as exc_info:
+ await __main__.main()
+
+ assert "API_HASH" in str(exc_info.value)
+
+ @pytest.mark.asyncio
+ async def test_main_invalid_api_id(self, monkeypatch_env, mock_telegram_client_context):
+ """Testa erro quando API_ID tem valor inválido."""
+ monkeypatch_env(API_ID="not_a_number", API_HASH="test_hash")
+
+ # Mock TelegramClient no módulo __main__ para evitar tentativa de conexão real
+ with patch(
+ "clean_telegram.__main__.TelegramClient", return_value=mock_telegram_client_context()
+ ):
+ # Mock load_dotenv para não carregar .env real
+ with patch("clean_telegram.__main__.load_dotenv"):
+ # Usar --dry-run para pular confirmação
+ with patch("sys.argv", ["clean-telegram", "--dry-run"]):
+ with pytest.raises(SystemExit) as exc_info:
+ await __main__.main()
+
+ # Mensagem de erro pode ser sobre API_ID inválido ou faltando
+ assert "API_ID" in str(exc_info.value)
+
+ @pytest.mark.asyncio
+ async def test_main_resolves_session_name(self, monkeypatch_env, mock_async_telegram_client):
+ """Testa se resolve_session_name é usado antes de criar TelegramClient."""
+ monkeypatch_env(API_ID="12345", API_HASH="test_hash", SESSION_NAME="session")
+
+ client = AsyncMock()
+ client.get_me = AsyncMock(
+ return_value=Mock(username="testuser", id=12345, first_name="Test")
+ )
+
+ async def mock_iter_dialogs():
+ return
+ yield
+
+ client.iter_dialogs = mock_iter_dialogs
+ mock_async_telegram_client.__aenter__.return_value = client
+
+ with patch("clean_telegram.__main__.load_dotenv"):
+ with patch("sys.argv", ["clean-telegram", "--dry-run"]):
+ with patch(
+ "clean_telegram.__main__.resolve_session_name",
+ return_value="/tmp/clean-telegram/session",
+ ) as resolve_session_name_mock:
+ with patch(
+ "clean_telegram.__main__.TelegramClient",
+ return_value=mock_async_telegram_client,
+ ) as telegram_client_mock:
+ await __main__.main()
+
+ resolve_session_name_mock.assert_called_once_with("session")
+ telegram_client_mock.assert_called_once_with(
+ "/tmp/clean-telegram/session", 12345, "test_hash"
+ )
+
+
+class TestMainConfirmation:
+ """Testes para fluxo de confirmação."""
+
+ @pytest.mark.asyncio
+ async def test_main_confirmation_cancel(self, monkeypatch_env, mock_async_telegram_client):
+ """Testa cancelamento quando usuário não digita 'APAGAR TUDO'."""
+ monkeypatch_env(API_ID="12345", API_HASH="test_hash")
+
+ # Mock stdin para retornar resposta diferente de "APAGAR TUDO"
+ mock_stdin = MagicMock()
+ mock_stdin.readline.return_value = "nao apagar\n"
+
+ with patch("sys.argv", ["clean-telegram"]):
+ with patch("sys.stdin", mock_stdin):
+ with patch(
+ "clean_telegram.__main__.TelegramClient",
+ return_value=mock_async_telegram_client,
+ ):
+ await __main__.main()
+
+ # Verifica que o client não foi usado (cancelado antes de entrar)
+ mock_async_telegram_client.__aenter__.assert_not_called()
+
+ @pytest.mark.asyncio
+ async def test_main_confirmation_accept(self, monkeypatch_env, mock_async_telegram_client):
+ """Testa confirmação quando usuário digita 'APAGAR TUDO'."""
+ monkeypatch_env(API_ID="12345", API_HASH="test_hash")
+
+ # Mock stdin para retornar "APAGAR TUDO"
+ mock_stdin = MagicMock()
+ mock_stdin.readline.return_value = "APAGAR TUDO\n"
+
+ # Mock iter_dialogs para não retornar nada (lista vazia)
+ client = AsyncMock()
+ client.get_me = AsyncMock(
+ return_value=Mock(username="testuser", id=12345, first_name="Test")
+ )
+
+ async def mock_iter_dialogs():
+ return
+ yield # Make it an async generator
+
+ client.iter_dialogs = mock_iter_dialogs
+ mock_async_telegram_client.__aenter__.return_value = client
+
+ with patch("sys.argv", ["clean-telegram"]):
+ with patch("sys.stdin", mock_stdin):
+ with patch(
+ "clean_telegram.__main__.TelegramClient",
+ return_value=mock_async_telegram_client,
+ ):
+ await __main__.main()
+
+ # Verifica que o client foi inicializado (confirmação aceita)
+ mock_async_telegram_client.__aenter__.assert_called_once()
+
+
+class TestMainDryRun:
+ """Testes para flag --dry-run."""
+
+ @pytest.mark.asyncio
+ async def test_main_dry_run_no_confirmation(self, monkeypatch_env, mock_async_telegram_client):
+ """Verifica que dry-run pula confirmação."""
+ monkeypatch_env(API_ID="12345", API_HASH="test_hash")
+
+ # Mock iter_dialogs para não retornar nada
+ client = AsyncMock()
+ client.get_me = AsyncMock(
+ return_value=Mock(username="testuser", id=12345, first_name="Test")
+ )
+
+ async def mock_iter_dialogs():
+ return
+ yield
+
+ client.iter_dialogs = mock_iter_dialogs
+ mock_async_telegram_client.__aenter__.return_value = client
+
+ with patch("sys.argv", ["clean-telegram", "--dry-run"]):
+ with patch(
+ "clean_telegram.__main__.TelegramClient", return_value=mock_async_telegram_client
+ ):
+ await __main__.main()
+
+ # Verifica que o client foi usado sem pedir confirmação
+ mock_async_telegram_client.__aenter__.assert_called_once()
+
+ @pytest.mark.asyncio
+ async def test_main_dry_run_flag(self, monkeypatch_env, mock_async_telegram_client):
+ """Testa execução em dry-run."""
+ monkeypatch_env(API_ID="12345", API_HASH="test_hash")
+
+ # Criar um mock de diálogo
+ mock_dialog = MagicMock()
+ mock_dialog.name = "Test Channel"
+ mock_dialog.entity = MagicMock(spec=Channel)
+
+ # Mock iter_dialogs como async generator
+ async def mock_iter_dialogs():
+ yield mock_dialog
+
+ client = AsyncMock()
+ client.get_me = AsyncMock(
+ return_value=Mock(username="testuser", id=12345, first_name="Test")
+ )
+ client.iter_dialogs = mock_iter_dialogs
+ mock_async_telegram_client.__aenter__.return_value = client
+
+ with patch("sys.argv", ["clean-telegram", "--dry-run"]):
+ with patch(
+ "clean_telegram.__main__.TelegramClient", return_value=mock_async_telegram_client
+ ):
+ await __main__.main()
+
+ # Em dry-run, as operações destrutivas não devem ser executidas
+ # client.delete_dialog não deve ser chamado para canais
+ client.delete_dialog.assert_not_called()
+
+
+class TestMainLimitFlag:
+ """Testes para flag --limit."""
+
+ @pytest.mark.asyncio
+ async def test_main_limit_flag(self, monkeypatch_env, mock_async_telegram_client):
+ """Testa flag --limit para processar subset de diálogos."""
+ monkeypatch_env(API_ID="12345", API_HASH="test_hash")
+
+ # Criar 5 mocks de diálogos
+ mock_dialogs = []
+ for i in range(5):
+ mock_dialog = MagicMock()
+ mock_dialog.name = f"Dialog {i}"
+ mock_dialog.entity = MagicMock(spec=Channel)
+ mock_dialogs.append(mock_dialog)
+
+ # Mock iter_dialogs como async generator
+ async def mock_iter_dialogs():
+ for dialog in mock_dialogs:
+ yield dialog
+
+ client = AsyncMock()
+ client.get_me = AsyncMock(
+ return_value=Mock(username="testuser", id=12345, first_name="Test")
+ )
+ client.iter_dialogs = mock_iter_dialogs
+ mock_async_telegram_client.__aenter__.return_value = client
+
+ with patch("sys.argv", ["clean-telegram", "--dry-run", "--limit", "2"]):
+ with patch(
+ "clean_telegram.__main__.TelegramClient", return_value=mock_async_telegram_client
+ ):
+ await __main__.main()
+
+ # Com --limit 2, apenas 2 diálogos devem ser processados
+ # Não há como verificar diretamente quantos foram processados,
+ # mas podemos verificar que o iter_dialogs foi chamado
+
+
+class TestMainYesFlag:
+ """Testes para flag --yes."""
+
+ @pytest.mark.asyncio
+ async def test_main_yes_flag(self, monkeypatch_env, mock_async_telegram_client):
+ """Testa flag --yes para pular confirmação."""
+ monkeypatch_env(API_ID="12345", API_HASH="test_hash")
+
+ # Mock iter_dialogs para não retornar nada
+ client = AsyncMock()
+ client.get_me = AsyncMock(
+ return_value=Mock(username="testuser", id=12345, first_name="Test")
+ )
+
+ async def mock_iter_dialogs():
+ return
+ yield
+
+ client.iter_dialogs = mock_iter_dialogs
+ mock_async_telegram_client.__aenter__.return_value = client
+
+ with patch("sys.argv", ["clean-telegram", "--yes"]):
+ with patch(
+ "clean_telegram.__main__.TelegramClient", return_value=mock_async_telegram_client
+ ):
+ await __main__.main()
+
+ # Verifica que o client foi usado sem pedir confirmação
+ mock_async_telegram_client.__aenter__.assert_called_once()
+
+
+class TestMainClientIteration:
+ """Testes para iteração sobre diálogos."""
+
+ @pytest.mark.asyncio
+ async def test_main_client_iteration_empty(self, monkeypatch_env, mock_async_telegram_client):
+ """Testa iteração quando não há diálogos."""
+ monkeypatch_env(API_ID="12345", API_HASH="test_hash")
+
+ # Mock iter_dialogs para não retornar nada
+ client = AsyncMock()
+ client.get_me = AsyncMock(
+ return_value=Mock(username="testuser", id=12345, first_name="Test")
+ )
+
+ async def mock_iter_dialogs():
+ return
+ yield
+
+ client.iter_dialogs = mock_iter_dialogs
+ mock_async_telegram_client.__aenter__.return_value = client
+
+ with patch("sys.argv", ["clean-telegram", "--yes"]):
+ with patch(
+ "clean_telegram.__main__.TelegramClient", return_value=mock_async_telegram_client
+ ):
+ await __main__.main()
+
+ # Verifica que get_me foi chamado
+ client.get_me.assert_called_once()
+
+ @pytest.mark.asyncio
+ async def test_main_client_iteration_with_dialogs(
+ self, monkeypatch_env, mock_async_telegram_client
+ ):
+ """Testa iteração sobre diálogos com mocks."""
+ monkeypatch_env(API_ID="12345", API_HASH="test_hash")
+
+ # Criar mocks de diferentes tipos de diálogos
+ mock_channel = MagicMock()
+ mock_channel.name = "Test Channel"
+ mock_channel.entity = MagicMock(spec=Channel)
+
+ mock_user = MagicMock()
+ mock_user.name = "Test User"
+ mock_user.entity = MagicMock(spec=User)
+
+ # Mock iter_dialogs como async generator
+ async def mock_iter_dialogs():
+ yield mock_channel
+ yield mock_user
+
+ client = AsyncMock()
+ client.get_me = AsyncMock(
+ return_value=Mock(username="testuser", id=12345, first_name="Test")
+ )
+ client.iter_dialogs = mock_iter_dialogs
+ mock_async_telegram_client.__aenter__.return_value = client
+
+ with patch("sys.argv", ["clean-telegram", "--dry-run"]):
+ with patch(
+ "clean_telegram.__main__.TelegramClient", return_value=mock_async_telegram_client
+ ):
+ await __main__.main()
+
+ # Verifica que get_me foi chamado
+ client.get_me.assert_called_once()
+
+
+class TestMainFloodWaitRetry:
+ """Testes para retry em FloodWaitError."""
+
+ @pytest.mark.asyncio
+ async def test_main_flood_wait_retry(self, monkeypatch_env, mock_async_telegram_client):
+ """Testa retry em FloodWaitError."""
+ monkeypatch_env(API_ID="12345", API_HASH="test_hash")
+
+ # Criar mock de diálogo
+ mock_dialog = MagicMock()
+ mock_dialog.name = "Test Channel"
+ mock_dialog.entity = MagicMock(spec=Channel)
+
+ # Contador para controlar quando lançar erro
+ call_count = {"count": 0}
+
+ # Mock process_dialog para lançar FloodWaitError na primeira chamada
+ async def mock_process_dialog(*args, **kwargs):
+ call_count["count"] += 1
+ if call_count["count"] == 1:
+ raise FloodWaitError(None, 1)
+ return True
+
+ # Mock iter_dialogs como async generator
+ async def mock_iter_dialogs():
+ yield mock_dialog
+
+ client = AsyncMock()
+ client.get_me = AsyncMock(
+ return_value=Mock(username="testuser", id=12345, first_name="Test")
+ )
+ client.iter_dialogs = mock_iter_dialogs
+ mock_async_telegram_client.__aenter__.return_value = client
+
+ with patch("sys.argv", ["clean-telegram", "--dry-run"]):
+ with patch(
+ "clean_telegram.__main__.TelegramClient", return_value=mock_async_telegram_client
+ ):
+ with patch(
+ "clean_telegram.__main__.process_dialog", side_effect=mock_process_dialog
+ ):
+ await __main__.main()
+
+ # Verifica que process_dialog foi chamado mais de uma vez (retry)
+ assert call_count["count"] >= 1
+
+ @pytest.mark.asyncio
+ async def test_main_max_retries_exceeded(self, monkeypatch_env, mock_async_telegram_client):
+ """Testa quando max retries é atingido."""
+ monkeypatch_env(API_ID="12345", API_HASH="test_hash")
+
+ # Criar mock de diálogo
+ mock_dialog = MagicMock()
+ mock_dialog.name = "Test Channel"
+ mock_dialog.entity = MagicMock(spec=Channel)
+
+ # Mock process_dialog para sempre lançar FloodWaitError
+ async def mock_process_dialog_failing(*args, **kwargs):
+ raise FloodWaitError(None, 1)
+
+ # Mock iter_dialogs como async generator
+ async def mock_iter_dialogs():
+ yield mock_dialog
+
+ client = AsyncMock()
+ client.get_me = AsyncMock(
+ return_value=Mock(username="testuser", id=12345, first_name="Test")
+ )
+ client.iter_dialogs = mock_iter_dialogs
+ mock_async_telegram_client.__aenter__.return_value = client
+
+ with patch("sys.argv", ["clean-telegram", "--dry-run"]):
+ with patch(
+ "clean_telegram.__main__.TelegramClient", return_value=mock_async_telegram_client
+ ):
+ with patch(
+ "clean_telegram.__main__.process_dialog",
+ side_effect=mock_process_dialog_failing,
+ ):
+ await __main__.main()
+
+ # Verifica que o loop continuou mesmo após max retries
+
+
+class TestMainRPCErrorHandling:
+ """Testes para tratamento de RPCError."""
+
+ @pytest.mark.asyncio
+ async def test_main_rpc_error_handling(self, monkeypatch_env, mock_async_telegram_client):
+ """Testa RPCError durante iteração."""
+ monkeypatch_env(API_ID="12345", API_HASH="test_hash")
+
+ # Criar mocks de diálogos
+ mock_dialog1 = MagicMock()
+ mock_dialog1.name = "Dialog 1"
+ mock_dialog1.entity = MagicMock(spec=Channel)
+
+ mock_dialog2 = MagicMock()
+ mock_dialog2.name = "Dialog 2"
+ mock_dialog2.entity = MagicMock(spec=Channel)
+
+ # Flag para controlar qual diálogo falha
+ call_count = {"count": 0}
+
+ # Mock process_dialog para lançar RPCError na primeira chamada
+ async def mock_process_dialog(*args, **kwargs):
+ call_count["count"] += 1
+ if call_count["count"] == 1:
+ raise RPCError(None, "Test error")
+ return True
+
+ # Mock iter_dialogs como async generator
+ async def mock_iter_dialogs():
+ yield mock_dialog1
+ yield mock_dialog2
+
+ client = AsyncMock()
+ client.get_me = AsyncMock(
+ return_value=Mock(username="testuser", id=12345, first_name="Test")
+ )
+ client.iter_dialogs = mock_iter_dialogs
+ mock_async_telegram_client.__aenter__.return_value = client
+
+ with patch("sys.argv", ["clean-telegram", "--dry-run"]):
+ with patch(
+ "clean_telegram.__main__.TelegramClient", return_value=mock_async_telegram_client
+ ):
+ with patch(
+ "clean_telegram.__main__.process_dialog", side_effect=mock_process_dialog
+ ):
+ await __main__.main()
+
+ # Verifica que ambos os diálogos foram considerados (mesmo com erro no primeiro)
+ # O segundo diálogo deve ter sido processado
+ assert call_count["count"] == 2
diff --git a/tests/test_utils.py b/tests/test_utils.py
new file mode 100644
index 0000000..954d137
--- /dev/null
+++ b/tests/test_utils.py
@@ -0,0 +1,179 @@
+"""Testes para o módulo utils.py."""
+
+import os
+
+import pytest
+
+from clean_telegram.utils import env_int, resolve_session_name, safe_sleep
+
+
+class TestEnvInt:
+ """Testes para env_int."""
+
+ def test_env_int_valid(self, temp_env_vars):
+ """Testa env_int com valor válido."""
+ os.environ["TEST_VAR"] = "12345"
+ result = env_int("TEST_VAR")
+ assert result == 12345
+
+ def test_env_int_missing(self, temp_env_vars):
+ """Testa env_int com variável não definida."""
+ with pytest.raises(SystemExit):
+ env_int("NONEXISTENT_VAR")
+
+ def test_env_int_invalid_value(self, temp_env_vars):
+ """Testa env_int com valor não numérico."""
+ os.environ["TEST_VAR"] = "not_a_number"
+ with pytest.raises(SystemExit):
+ env_int("TEST_VAR")
+
+ def test_env_int_zero(self, temp_env_vars):
+ """Testa env_int com valor zero."""
+ os.environ["TEST_VAR"] = "0"
+ result = env_int("TEST_VAR")
+ assert result == 0
+
+ def test_env_int_negative(self, temp_env_vars):
+ """Testa env_int com valor negativo."""
+ os.environ["TEST_VAR"] = "-100"
+ result = env_int("TEST_VAR")
+ assert result == -100
+
+ def test_env_int_empty_string(self, temp_env_vars):
+ """Testa env_int com string vazia."""
+ os.environ["TEST_VAR"] = ""
+ with pytest.raises(SystemExit):
+ env_int("TEST_VAR")
+
+ def test_env_int_spaces(self, temp_env_vars):
+ """Testa env_int com espaços que devem ser removidos."""
+ os.environ["TEST_VAR"] = " 123 "
+ result = env_int("TEST_VAR")
+ assert result == 123
+
+ def test_env_int_float_string(self, temp_env_vars):
+ """Testa env_int com string de float (deve falhar)."""
+ os.environ["TEST_VAR"] = "12.5"
+ with pytest.raises(SystemExit):
+ env_int("TEST_VAR")
+
+
+class TestSafeSleep:
+ """Testes para safe_sleep."""
+
+ @pytest.mark.asyncio
+ async def test_safe_sleep_zero(self):
+ """Testa safe_sleep com zero segundos."""
+ await safe_sleep(0)
+ # Não deve levantar exceção
+
+ @pytest.mark.asyncio
+ async def test_safe_sleep_positive(self):
+ """Testa safe_sleep com valor positivo."""
+ import time
+
+ start = time.monotonic()
+ await safe_sleep(0.1)
+ elapsed = time.monotonic() - start
+ assert elapsed >= 0.1
+
+ @pytest.mark.asyncio
+ async def test_safe_sleep_negative(self):
+ """Testa safe_sleep com valor negativo (deve falhar)."""
+ with pytest.raises(ValueError, match="não negativo"):
+ await safe_sleep(-1)
+
+ @pytest.mark.asyncio
+ async def test_safe_sleep_invalid_type(self):
+ """Testa safe_sleep com tipo inválido (deve falhar)."""
+ with pytest.raises(ValueError, match="número"):
+ await safe_sleep("invalid")
+
+ @pytest.mark.asyncio
+ async def test_safe_sleep_float(self):
+ """Testa safe_sleep com valor float."""
+ import time
+
+ start = time.monotonic()
+ await safe_sleep(0.05)
+ elapsed = time.monotonic() - start
+ assert elapsed >= 0.05
+
+
+class TestResolveSessionName:
+ """Testes para resolução do caminho de sessão."""
+
+ def test_resolve_session_name_defaults_to_user_dir(self, tmp_path):
+ """Nome simples deve usar ~/.clean_telegram."""
+ cwd = tmp_path / "cwd"
+ home = tmp_path / "home"
+ cwd.mkdir()
+ home.mkdir()
+
+ result = resolve_session_name("session", cwd=cwd, home=home)
+
+ assert result == str(home / ".clean_telegram" / "session")
+ assert (home / ".clean_telegram").exists()
+
+ def test_resolve_session_name_uses_absolute_path(self, tmp_path):
+ """Caminho absoluto deve ser preservado."""
+ cwd = tmp_path / "cwd"
+ home = tmp_path / "home"
+ cwd.mkdir()
+ home.mkdir()
+ absolute = tmp_path / "sessions" / "my_session"
+
+ result = resolve_session_name(str(absolute), cwd=cwd, home=home)
+
+ assert result == str(absolute)
+ assert absolute.parent.exists()
+
+ def test_resolve_session_name_uses_relative_directory(self, tmp_path):
+ """Caminho relativo com diretório deve ser resolvido pelo cwd."""
+ cwd = tmp_path / "cwd"
+ home = tmp_path / "home"
+ cwd.mkdir()
+ home.mkdir()
+
+ result = resolve_session_name("data/my_session", cwd=cwd, home=home)
+
+ assert result == str(cwd / "data" / "my_session")
+ assert (cwd / "data").exists()
+
+ def test_resolve_session_name_supports_dot_relative_path(self, tmp_path):
+ """Caminho relativo com ./ deve ser tratado como caminho explícito."""
+ cwd = tmp_path / "cwd"
+ home = tmp_path / "home"
+ cwd.mkdir()
+ home.mkdir()
+
+ result = resolve_session_name("./session", cwd=cwd, home=home)
+
+ assert result == str(cwd / "session")
+
+ def test_resolve_session_name_migrates_legacy_file(self, tmp_path):
+ """Sessão legada local deve ser migrada para ~/.clean_telegram."""
+ cwd = tmp_path / "cwd"
+ home = tmp_path / "home"
+ cwd.mkdir()
+ home.mkdir()
+ legacy = cwd / "session.session"
+ legacy.write_text("legacy-data", encoding="utf-8")
+
+ result = resolve_session_name("session", cwd=cwd, home=home)
+
+ modern = home / ".clean_telegram" / "session.session"
+ assert result == str(home / ".clean_telegram" / "session")
+ assert modern.exists()
+ assert modern.read_text(encoding="utf-8") == "legacy-data"
+
+ def test_resolve_session_name_defaults_when_empty(self, tmp_path):
+ """Valor vazio deve cair no nome padrão 'session'."""
+ cwd = tmp_path / "cwd"
+ home = tmp_path / "home"
+ cwd.mkdir()
+ home.mkdir()
+
+ result = resolve_session_name(" ", cwd=cwd, home=home)
+
+ assert result == str(home / ".clean_telegram" / "session")
diff --git a/uv.lock b/uv.lock
new file mode 100644
index 0000000..9959696
--- /dev/null
+++ b/uv.lock
@@ -0,0 +1,670 @@
+version = 1
+revision = 3
+requires-python = ">=3.10"
+
+[[package]]
+name = "backports-asyncio-runner"
+version = "1.2.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/8e/ff/70dca7d7cb1cbc0edb2c6cc0c38b65cba36cccc491eca64cabd5fe7f8670/backports_asyncio_runner-1.2.0.tar.gz", hash = "sha256:a5aa7b2b7d8f8bfcaa2b57313f70792df84e32a2a746f585213373f900b42162", size = 69893, upload-time = "2025-07-02T02:27:15.685Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/a0/59/76ab57e3fe74484f48a53f8e337171b4a2349e506eabe136d7e01d059086/backports_asyncio_runner-1.2.0-py3-none-any.whl", hash = "sha256:0da0a936a8aeb554eccb426dc55af3ba63bcdc69fa1a600b5bb305413a4477b5", size = 12313, upload-time = "2025-07-02T02:27:14.263Z" },
+]
+
+[[package]]
+name = "black"
+version = "26.1.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "click" },
+ { name = "mypy-extensions" },
+ { name = "packaging" },
+ { name = "pathspec" },
+ { name = "platformdirs" },
+ { name = "pytokens" },
+ { name = "tomli", marker = "python_full_version < '3.11'" },
+ { name = "typing-extensions", marker = "python_full_version < '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/13/88/560b11e521c522440af991d46848a2bde64b5f7202ec14e1f46f9509d328/black-26.1.0.tar.gz", hash = "sha256:d294ac3340eef9c9eb5d29288e96dc719ff269a88e27b396340459dd85da4c58", size = 658785, upload-time = "2026-01-18T04:50:11.993Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/51/1b/523329e713f965ad0ea2b7a047eeb003007792a0353622ac7a8cb2ee6fef/black-26.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ca699710dece84e3ebf6e92ee15f5b8f72870ef984bf944a57a777a48357c168", size = 1849661, upload-time = "2026-01-18T04:59:12.425Z" },
+ { url = "https://files.pythonhosted.org/packages/14/82/94c0640f7285fa71c2f32879f23e609dd2aa39ba2641f395487f24a578e7/black-26.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5e8e75dabb6eb83d064b0db46392b25cabb6e784ea624219736e8985a6b3675d", size = 1689065, upload-time = "2026-01-18T04:59:13.993Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/78/474373cbd798f9291ed8f7107056e343fd39fef42de4a51c7fd0d360840c/black-26.1.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eb07665d9a907a1a645ee41a0df8a25ffac8ad9c26cdb557b7b88eeeeec934e0", size = 1751502, upload-time = "2026-01-18T04:59:15.971Z" },
+ { url = "https://files.pythonhosted.org/packages/29/89/59d0e350123f97bc32c27c4d79563432d7f3530dca2bff64d855c178af8b/black-26.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:7ed300200918147c963c87700ccf9966dceaefbbb7277450a8d646fc5646bf24", size = 1400102, upload-time = "2026-01-18T04:59:17.8Z" },
+ { url = "https://files.pythonhosted.org/packages/e1/bc/5d866c7ae1c9d67d308f83af5462ca7046760158bbf142502bad8f22b3a1/black-26.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:c5b7713daea9bf943f79f8c3b46f361cc5229e0e604dcef6a8bb6d1c37d9df89", size = 1207038, upload-time = "2026-01-18T04:59:19.543Z" },
+ { url = "https://files.pythonhosted.org/packages/30/83/f05f22ff13756e1a8ce7891db517dbc06200796a16326258268f4658a745/black-26.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3cee1487a9e4c640dc7467aaa543d6c0097c391dc8ac74eb313f2fbf9d7a7cb5", size = 1831956, upload-time = "2026-01-18T04:59:21.38Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/f2/b2c570550e39bedc157715e43927360312d6dd677eed2cc149a802577491/black-26.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d62d14ca31c92adf561ebb2e5f2741bf8dea28aef6deb400d49cca011d186c68", size = 1672499, upload-time = "2026-01-18T04:59:23.257Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/d7/990d6a94dc9e169f61374b1c3d4f4dd3037e93c2cc12b6f3b12bc663aa7b/black-26.1.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fb1dafbbaa3b1ee8b4550a84425aac8874e5f390200f5502cf3aee4a2acb2f14", size = 1735431, upload-time = "2026-01-18T04:59:24.729Z" },
+ { url = "https://files.pythonhosted.org/packages/36/1c/cbd7bae7dd3cb315dfe6eeca802bb56662cc92b89af272e014d98c1f2286/black-26.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:101540cb2a77c680f4f80e628ae98bd2bd8812fb9d72ade4f8995c5ff019e82c", size = 1400468, upload-time = "2026-01-18T04:59:27.381Z" },
+ { url = "https://files.pythonhosted.org/packages/59/b1/9fe6132bb2d0d1f7094613320b56297a108ae19ecf3041d9678aec381b37/black-26.1.0-cp311-cp311-win_arm64.whl", hash = "sha256:6f3977a16e347f1b115662be07daa93137259c711e526402aa444d7a88fdc9d4", size = 1207332, upload-time = "2026-01-18T04:59:28.711Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/13/710298938a61f0f54cdb4d1c0baeb672c01ff0358712eddaf29f76d32a0b/black-26.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6eeca41e70b5f5c84f2f913af857cf2ce17410847e1d54642e658e078da6544f", size = 1878189, upload-time = "2026-01-18T04:59:30.682Z" },
+ { url = "https://files.pythonhosted.org/packages/79/a6/5179beaa57e5dbd2ec9f1c64016214057b4265647c62125aa6aeffb05392/black-26.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:dd39eef053e58e60204f2cdf059e2442e2eb08f15989eefe259870f89614c8b6", size = 1700178, upload-time = "2026-01-18T04:59:32.387Z" },
+ { url = "https://files.pythonhosted.org/packages/8c/04/c96f79d7b93e8f09d9298b333ca0d31cd9b2ee6c46c274fd0f531de9dc61/black-26.1.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9459ad0d6cd483eacad4c6566b0f8e42af5e8b583cee917d90ffaa3778420a0a", size = 1777029, upload-time = "2026-01-18T04:59:33.767Z" },
+ { url = "https://files.pythonhosted.org/packages/49/f9/71c161c4c7aa18bdda3776b66ac2dc07aed62053c7c0ff8bbda8c2624fe2/black-26.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:a19915ec61f3a8746e8b10adbac4a577c6ba9851fa4a9e9fbfbcf319887a5791", size = 1406466, upload-time = "2026-01-18T04:59:35.177Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/8b/a7b0f974e473b159d0ac1b6bcefffeb6bec465898a516ee5cc989503cbc7/black-26.1.0-cp312-cp312-win_arm64.whl", hash = "sha256:643d27fb5facc167c0b1b59d0315f2674a6e950341aed0fc05cf307d22bf4954", size = 1216393, upload-time = "2026-01-18T04:59:37.18Z" },
+ { url = "https://files.pythonhosted.org/packages/79/04/fa2f4784f7237279332aa735cdfd5ae2e7730db0072fb2041dadda9ae551/black-26.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ba1d768fbfb6930fc93b0ecc32a43d8861ded16f47a40f14afa9bb04ab93d304", size = 1877781, upload-time = "2026-01-18T04:59:39.054Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/ad/5a131b01acc0e5336740a039628c0ab69d60cf09a2c87a4ec49f5826acda/black-26.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2b807c240b64609cb0e80d2200a35b23c7df82259f80bef1b2c96eb422b4aac9", size = 1699670, upload-time = "2026-01-18T04:59:41.005Z" },
+ { url = "https://files.pythonhosted.org/packages/da/7c/b05f22964316a52ab6b4265bcd52c0ad2c30d7ca6bd3d0637e438fc32d6e/black-26.1.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1de0f7d01cc894066a1153b738145b194414cc6eeaad8ef4397ac9abacf40f6b", size = 1775212, upload-time = "2026-01-18T04:59:42.545Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/a3/e8d1526bea0446e040193185353920a9506eab60a7d8beb062029129c7d2/black-26.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:91a68ae46bf07868963671e4d05611b179c2313301bd756a89ad4e3b3db2325b", size = 1409953, upload-time = "2026-01-18T04:59:44.357Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/5a/d62ebf4d8f5e3a1daa54adaab94c107b57be1b1a2f115a0249b41931e188/black-26.1.0-cp313-cp313-win_arm64.whl", hash = "sha256:be5e2fe860b9bd9edbf676d5b60a9282994c03fbbd40fe8f5e75d194f96064ca", size = 1217707, upload-time = "2026-01-18T04:59:45.719Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/83/be35a175aacfce4b05584ac415fd317dd6c24e93a0af2dcedce0f686f5d8/black-26.1.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:9dc8c71656a79ca49b8d3e2ce8103210c9481c57798b48deeb3a8bb02db5f115", size = 1871864, upload-time = "2026-01-18T04:59:47.586Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/f5/d33696c099450b1274d925a42b7a030cd3ea1f56d72e5ca8bbed5f52759c/black-26.1.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b22b3810451abe359a964cc88121d57f7bce482b53a066de0f1584988ca36e79", size = 1701009, upload-time = "2026-01-18T04:59:49.443Z" },
+ { url = "https://files.pythonhosted.org/packages/1b/87/670dd888c537acb53a863bc15abbd85b22b429237d9de1b77c0ed6b79c42/black-26.1.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:53c62883b3f999f14e5d30b5a79bd437236658ad45b2f853906c7cbe79de00af", size = 1767806, upload-time = "2026-01-18T04:59:50.769Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/9c/cd3deb79bfec5bcf30f9d2100ffeec63eecce826eb63e3961708b9431ff1/black-26.1.0-cp314-cp314-win_amd64.whl", hash = "sha256:f016baaadc423dc960cdddf9acae679e71ee02c4c341f78f3179d7e4819c095f", size = 1433217, upload-time = "2026-01-18T04:59:52.218Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/29/f3be41a1cf502a283506f40f5d27203249d181f7a1a2abce1c6ce188035a/black-26.1.0-cp314-cp314-win_arm64.whl", hash = "sha256:66912475200b67ef5a0ab665011964bf924745103f51977a78b4fb92a9fc1bf0", size = 1245773, upload-time = "2026-01-18T04:59:54.457Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/3d/51bdb3ecbfadfaf825ec0c75e1de6077422b4afa2091c6c9ba34fbfc0c2d/black-26.1.0-py3-none-any.whl", hash = "sha256:1054e8e47ebd686e078c0bb0eaf31e6ce69c966058d122f2c0c950311f9f3ede", size = 204010, upload-time = "2026-01-18T04:50:09.978Z" },
+]
+
+[[package]]
+name = "clean-telegram"
+version = "0.1.0"
+source = { editable = "." }
+dependencies = [
+ { name = "python-dotenv" },
+ { name = "qrcode" },
+ { name = "telethon" },
+]
+
+[package.optional-dependencies]
+dev = [
+ { name = "black" },
+ { name = "flake8" },
+ { name = "isort" },
+ { name = "mypy" },
+ { name = "pytest" },
+ { name = "pytest-asyncio" },
+ { name = "pytest-cov" },
+]
+
+[package.metadata]
+requires-dist = [
+ { name = "black", marker = "extra == 'dev'", specifier = ">=24.0.0" },
+ { name = "flake8", marker = "extra == 'dev'", specifier = ">=7.0.0" },
+ { name = "isort", marker = "extra == 'dev'", specifier = ">=5.13.0" },
+ { name = "mypy", marker = "extra == 'dev'", specifier = ">=1.8.0" },
+ { name = "pytest", marker = "extra == 'dev'", specifier = ">=8.0.0" },
+ { name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=0.23.0" },
+ { name = "pytest-cov", marker = "extra == 'dev'", specifier = ">=4.1.0" },
+ { name = "python-dotenv", specifier = ">=1.0.0,<2.0.0" },
+ { name = "qrcode", specifier = ">=8.0,<9.0" },
+ { name = "telethon", specifier = ">=1.42.0,<2.0.0" },
+]
+provides-extras = ["dev"]
+
+[[package]]
+name = "click"
+version = "8.3.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "colorama", marker = "sys_platform == 'win32'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/3d/fa/656b739db8587d7b5dfa22e22ed02566950fbfbcdc20311993483657a5c0/click-8.3.1.tar.gz", hash = "sha256:12ff4785d337a1bb490bb7e9c2b1ee5da3112e94a8622f26a6c77f5d2fc6842a", size = 295065, upload-time = "2025-11-15T20:45:42.706Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/98/78/01c019cdb5d6498122777c1a43056ebb3ebfeef2076d9d026bfe15583b2b/click-8.3.1-py3-none-any.whl", hash = "sha256:981153a64e25f12d547d3426c367a4857371575ee7ad18df2a6183ab0545b2a6", size = 108274, upload-time = "2025-11-15T20:45:41.139Z" },
+]
+
+[[package]]
+name = "colorama"
+version = "0.4.6"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
+]
+
+[[package]]
+name = "coverage"
+version = "7.13.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/11/43/3e4ac666cc35f231fa70c94e9f38459299de1a152813f9d2f60fc5f3ecaf/coverage-7.13.3.tar.gz", hash = "sha256:f7f6182d3dfb8802c1747eacbfe611b669455b69b7c037484bb1efbbb56711ac", size = 826832, upload-time = "2026-02-03T14:02:30.944Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ab/07/1c8099563a8a6c389a31c2d0aa1497cee86d6248bb4b9ba5e779215db9f9/coverage-7.13.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0b4f345f7265cdbdb5ec2521ffff15fa49de6d6c39abf89fc7ad68aa9e3a55f0", size = 219143, upload-time = "2026-02-03T13:59:40.459Z" },
+ { url = "https://files.pythonhosted.org/packages/69/39/a892d44af7aa092cab70e0cc5cdbba18eeccfe1d6930695dab1742eef9e9/coverage-7.13.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:96c3be8bae9d0333e403cc1a8eb078a7f928b5650bae94a18fb4820cc993fb9b", size = 219663, upload-time = "2026-02-03T13:59:41.951Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/25/9669dcf4c2bb4c3861469e6db20e52e8c11908cf53c14ec9b12e9fd4d602/coverage-7.13.3-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d6f4a21328ea49d38565b55599e1c02834e76583a6953e5586d65cb1efebd8f8", size = 246424, upload-time = "2026-02-03T13:59:43.418Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/68/d9766c4e298aca62ea5d9543e1dd1e4e1439d7284815244d8b7db1840bfb/coverage-7.13.3-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fc970575799a9d17d5c3fafd83a0f6ccf5d5117cdc9ad6fbd791e9ead82418b0", size = 248228, upload-time = "2026-02-03T13:59:44.816Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/e2/eea6cb4a4bd443741adf008d4cccec83a1f75401df59b6559aca2bdd9710/coverage-7.13.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:87ff33b652b3556b05e204ae20793d1f872161b0fa5ec8a9ac76f8430e152ed6", size = 250103, upload-time = "2026-02-03T13:59:46.271Z" },
+ { url = "https://files.pythonhosted.org/packages/db/77/664280ecd666c2191610842177e2fab9e5dbdeef97178e2078fed46a3d2c/coverage-7.13.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7df8759ee57b9f3f7b66799b7660c282f4375bef620ade1686d6a7b03699e75f", size = 247107, upload-time = "2026-02-03T13:59:48.53Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/df/2a672eab99e0d0eba52d8a63e47dc92245eee26954d1b2d3c8f7d372151f/coverage-7.13.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:f45c9bcb16bee25a798ccba8a2f6a1251b19de6a0d617bb365d7d2f386c4e20e", size = 248143, upload-time = "2026-02-03T13:59:50.027Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/dc/a104e7a87c13e57a358b8b9199a8955676e1703bb372d79722b54978ae45/coverage-7.13.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:318b2e4753cbf611061e01b6cc81477e1cdfeb69c36c4a14e6595e674caadb56", size = 246148, upload-time = "2026-02-03T13:59:52.025Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/89/e113d3a58dc20b03b7e59aed1e53ebc9ca6167f961876443e002b10e3ae9/coverage-7.13.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:24db3959de8ee394eeeca89ccb8ba25305c2da9a668dd44173394cbd5aa0777f", size = 246414, upload-time = "2026-02-03T13:59:53.859Z" },
+ { url = "https://files.pythonhosted.org/packages/3f/60/a3fd0a6e8d89b488396019a2268b6a1f25ab56d6d18f3be50f35d77b47dc/coverage-7.13.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:be14d0622125edef21b3a4d8cd2d138c4872bf6e38adc90fd92385e3312f406a", size = 247023, upload-time = "2026-02-03T13:59:55.454Z" },
+ { url = "https://files.pythonhosted.org/packages/19/fa/de4840bb939dbb22ba0648a6d8069fa91c9cf3b3fca8b0d1df461e885b3d/coverage-7.13.3-cp310-cp310-win32.whl", hash = "sha256:53be4aab8ddef18beb6188f3a3fdbf4d1af2277d098d4e618be3a8e6c88e74be", size = 221751, upload-time = "2026-02-03T13:59:57.383Z" },
+ { url = "https://files.pythonhosted.org/packages/de/87/233ff8b7ef62fb63f58c78623b50bef69681111e0c4d43504f422d88cda4/coverage-7.13.3-cp310-cp310-win_amd64.whl", hash = "sha256:bfeee64ad8b4aae3233abb77eb6b52b51b05fa89da9645518671b9939a78732b", size = 222686, upload-time = "2026-02-03T13:59:58.825Z" },
+ { url = "https://files.pythonhosted.org/packages/ec/09/1ac74e37cf45f17eb41e11a21854f7f92a4c2d6c6098ef4a1becb0c6d8d3/coverage-7.13.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5907605ee20e126eeee2abe14aae137043c2c8af2fa9b38d2ab3b7a6b8137f73", size = 219276, upload-time = "2026-02-03T14:00:00.296Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/cb/71908b08b21beb2c437d0d5870c4ec129c570ca1b386a8427fcdb11cf89c/coverage-7.13.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a88705500988c8acad8b8fd86c2a933d3aa96bec1ddc4bc5cb256360db7bbd00", size = 219776, upload-time = "2026-02-03T14:00:02.414Z" },
+ { url = "https://files.pythonhosted.org/packages/09/85/c4f3dd69232887666a2c0394d4be21c60ea934d404db068e6c96aa59cd87/coverage-7.13.3-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7bbb5aa9016c4c29e3432e087aa29ebee3f8fda089cfbfb4e6d64bd292dcd1c2", size = 250196, upload-time = "2026-02-03T14:00:04.197Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/cc/560ad6f12010344d0778e268df5ba9aa990aacccc310d478bf82bf3d302c/coverage-7.13.3-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0c2be202a83dde768937a61cdc5d06bf9fb204048ca199d93479488e6247656c", size = 252111, upload-time = "2026-02-03T14:00:05.639Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/66/3193985fb2c58e91f94cfbe9e21a6fdf941e9301fe2be9e92c072e9c8f8c/coverage-7.13.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f45e32ef383ce56e0ca099b2e02fcdf7950be4b1b56afaab27b4ad790befe5b", size = 254217, upload-time = "2026-02-03T14:00:07.738Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/78/f0f91556bf1faa416792e537c523c5ef9db9b1d32a50572c102b3d7c45b3/coverage-7.13.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6ed2e787249b922a93cd95c671cc9f4c9797a106e81b455c83a9ddb9d34590c0", size = 250318, upload-time = "2026-02-03T14:00:09.224Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/aa/fc654e45e837d137b2c1f3a2cc09b4aea1e8b015acd2f774fa0f3d2ddeba/coverage-7.13.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:05dd25b21afffe545e808265897c35f32d3e4437663923e0d256d9ab5031fb14", size = 251909, upload-time = "2026-02-03T14:00:10.712Z" },
+ { url = "https://files.pythonhosted.org/packages/73/4d/ab53063992add8a9ca0463c9d92cce5994a29e17affd1c2daa091b922a93/coverage-7.13.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:46d29926349b5c4f1ea4fca95e8c892835515f3600995a383fa9a923b5739ea4", size = 249971, upload-time = "2026-02-03T14:00:12.402Z" },
+ { url = "https://files.pythonhosted.org/packages/29/25/83694b81e46fcff9899694a1b6f57573429cdd82b57932f09a698f03eea5/coverage-7.13.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:fae6a21537519c2af00245e834e5bf2884699cc7c1055738fd0f9dc37a3644ad", size = 249692, upload-time = "2026-02-03T14:00:13.868Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/ef/d68fc304301f4cb4bf6aefa0045310520789ca38dabdfba9dbecd3f37919/coverage-7.13.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c672d4e2f0575a4ca2bf2aa0c5ced5188220ab806c1bb6d7179f70a11a017222", size = 250597, upload-time = "2026-02-03T14:00:15.461Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/85/240ad396f914df361d0f71e912ddcedb48130c71b88dc4193fe3c0306f00/coverage-7.13.3-cp311-cp311-win32.whl", hash = "sha256:fcda51c918c7a13ad93b5f89a58d56e3a072c9e0ba5c231b0ed81404bf2648fb", size = 221773, upload-time = "2026-02-03T14:00:17.462Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/71/165b3a6d3d052704a9ab52d11ea64ef3426745de517dda44d872716213a7/coverage-7.13.3-cp311-cp311-win_amd64.whl", hash = "sha256:d1a049b5c51b3b679928dd35e47c4a2235e0b6128b479a7596d0ef5b42fa6301", size = 222711, upload-time = "2026-02-03T14:00:19.449Z" },
+ { url = "https://files.pythonhosted.org/packages/51/d0/0ddc9c5934cdd52639c5df1f1eb0fdab51bb52348f3a8d1c7db9c600d93a/coverage-7.13.3-cp311-cp311-win_arm64.whl", hash = "sha256:79f2670c7e772f4917895c3d89aad59e01f3dbe68a4ed2d0373b431fad1dcfba", size = 221377, upload-time = "2026-02-03T14:00:20.968Z" },
+ { url = "https://files.pythonhosted.org/packages/94/44/330f8e83b143f6668778ed61d17ece9dc48459e9e74669177de02f45fec5/coverage-7.13.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ed48b4170caa2c4420e0cd27dc977caaffc7eecc317355751df8373dddcef595", size = 219441, upload-time = "2026-02-03T14:00:22.585Z" },
+ { url = "https://files.pythonhosted.org/packages/08/e7/29db05693562c2e65bdf6910c0af2fd6f9325b8f43caf7a258413f369e30/coverage-7.13.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8f2adf4bcffbbec41f366f2e6dffb9d24e8172d16e91da5799c9b7ed6b5716e6", size = 219801, upload-time = "2026-02-03T14:00:24.186Z" },
+ { url = "https://files.pythonhosted.org/packages/90/ae/7f8a78249b02b0818db46220795f8ac8312ea4abd1d37d79ea81db5cae81/coverage-7.13.3-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:01119735c690786b6966a1e9f098da4cd7ca9174c4cfe076d04e653105488395", size = 251306, upload-time = "2026-02-03T14:00:25.798Z" },
+ { url = "https://files.pythonhosted.org/packages/62/71/a18a53d1808e09b2e9ebd6b47dad5e92daf4c38b0686b4c4d1b2f3e42b7f/coverage-7.13.3-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8bb09e83c603f152d855f666d70a71765ca8e67332e5829e62cb9466c176af23", size = 254051, upload-time = "2026-02-03T14:00:27.474Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/0a/eb30f6455d04c5a3396d0696cad2df0269ae7444bb322f86ffe3376f7bf9/coverage-7.13.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b607a40cba795cfac6d130220d25962931ce101f2f478a29822b19755377fb34", size = 255160, upload-time = "2026-02-03T14:00:29.024Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/7e/a45baac86274ce3ed842dbb84f14560c673ad30535f397d89164ec56c5df/coverage-7.13.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:44f14a62f5da2e9aedf9080e01d2cda61df39197d48e323538ec037336d68da8", size = 251709, upload-time = "2026-02-03T14:00:30.641Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/df/dd0dc12f30da11349993f3e218901fdf82f45ee44773596050c8f5a1fb25/coverage-7.13.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:debf29e0b157769843dff0981cc76f79e0ed04e36bb773c6cac5f6029054bd8a", size = 253083, upload-time = "2026-02-03T14:00:32.14Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/32/fc764c8389a8ce95cb90eb97af4c32f392ab0ac23ec57cadeefb887188d3/coverage-7.13.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:824bb95cd71604031ae9a48edb91fd6effde669522f960375668ed21b36e3ec4", size = 251227, upload-time = "2026-02-03T14:00:34.721Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/ca/d025e9da8f06f24c34d2da9873957cfc5f7e0d67802c3e34d0caa8452130/coverage-7.13.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:8f1010029a5b52dc427c8e2a8dbddb2303ddd180b806687d1acd1bb1d06649e7", size = 250794, upload-time = "2026-02-03T14:00:36.278Z" },
+ { url = "https://files.pythonhosted.org/packages/45/c7/76bf35d5d488ec8f68682eb8e7671acc50a6d2d1c1182de1d2b6d4ffad3b/coverage-7.13.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cd5dee4fd7659d8306ffa79eeaaafd91fa30a302dac3af723b9b469e549247e0", size = 252671, upload-time = "2026-02-03T14:00:38.368Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/10/1921f1a03a7c209e1cb374f81a6b9b68b03cdb3ecc3433c189bc90e2a3d5/coverage-7.13.3-cp312-cp312-win32.whl", hash = "sha256:f7f153d0184d45f3873b3ad3ad22694fd73aadcb8cdbc4337ab4b41ea6b4dff1", size = 221986, upload-time = "2026-02-03T14:00:40.442Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/7c/f5d93297f8e125a80c15545edc754d93e0ed8ba255b65e609b185296af01/coverage-7.13.3-cp312-cp312-win_amd64.whl", hash = "sha256:03a6e5e1e50819d6d7436f5bc40c92ded7e484e400716886ac921e35c133149d", size = 222793, upload-time = "2026-02-03T14:00:42.106Z" },
+ { url = "https://files.pythonhosted.org/packages/43/59/c86b84170015b4555ebabca8649bdf9f4a1f737a73168088385ed0f947c4/coverage-7.13.3-cp312-cp312-win_arm64.whl", hash = "sha256:51c4c42c0e7d09a822b08b6cf79b3c4db8333fffde7450da946719ba0d45730f", size = 221410, upload-time = "2026-02-03T14:00:43.726Z" },
+ { url = "https://files.pythonhosted.org/packages/81/f3/4c333da7b373e8c8bfb62517e8174a01dcc373d7a9083698e3b39d50d59c/coverage-7.13.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:853c3d3c79ff0db65797aad79dee6be020efd218ac4510f15a205f1e8d13ce25", size = 219468, upload-time = "2026-02-03T14:00:45.829Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/31/0714337b7d23630c8de2f4d56acf43c65f8728a45ed529b34410683f7217/coverage-7.13.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f75695e157c83d374f88dcc646a60cb94173304a9258b2e74ba5a66b7614a51a", size = 219839, upload-time = "2026-02-03T14:00:47.407Z" },
+ { url = "https://files.pythonhosted.org/packages/12/99/bd6f2a2738144c98945666f90cae446ed870cecf0421c767475fcf42cdbe/coverage-7.13.3-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2d098709621d0819039f3f1e471ee554f55a0b2ac0d816883c765b14129b5627", size = 250828, upload-time = "2026-02-03T14:00:49.029Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/99/97b600225fbf631e6f5bfd3ad5bcaf87fbb9e34ff87492e5a572ff01bbe2/coverage-7.13.3-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:16d23d6579cf80a474ad160ca14d8b319abaa6db62759d6eef53b2fc979b58c8", size = 253432, upload-time = "2026-02-03T14:00:50.655Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/5c/abe2b3490bda26bd4f5e3e799be0bdf00bd81edebedc2c9da8d3ef288fa8/coverage-7.13.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:00d34b29a59d2076e6f318b30a00a69bf63687e30cd882984ed444e753990cc1", size = 254672, upload-time = "2026-02-03T14:00:52.757Z" },
+ { url = "https://files.pythonhosted.org/packages/31/ba/5d1957c76b40daff53971fe0adb84d9c2162b614280031d1d0653dd010c1/coverage-7.13.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ab6d72bffac9deb6e6cb0f61042e748de3f9f8e98afb0375a8e64b0b6e11746b", size = 251050, upload-time = "2026-02-03T14:00:54.332Z" },
+ { url = "https://files.pythonhosted.org/packages/69/dc/dffdf3bfe9d32090f047d3c3085378558cb4eb6778cda7de414ad74581ed/coverage-7.13.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e129328ad1258e49cae0123a3b5fcb93d6c2fa90d540f0b4c7cdcdc019aaa3dc", size = 252801, upload-time = "2026-02-03T14:00:56.121Z" },
+ { url = "https://files.pythonhosted.org/packages/87/51/cdf6198b0f2746e04511a30dc9185d7b8cdd895276c07bdb538e37f1cd50/coverage-7.13.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2213a8d88ed35459bda71597599d4eec7c2ebad201c88f0bfc2c26fd9b0dd2ea", size = 250763, upload-time = "2026-02-03T14:00:58.719Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/1a/596b7d62218c1d69f2475b69cc6b211e33c83c902f38ee6ae9766dd422da/coverage-7.13.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:00dd3f02de6d5f5c9c3d95e3e036c3c2e2a669f8bf2d3ceb92505c4ce7838f67", size = 250587, upload-time = "2026-02-03T14:01:01.197Z" },
+ { url = "https://files.pythonhosted.org/packages/f7/46/52330d5841ff660f22c130b75f5e1dd3e352c8e7baef5e5fef6b14e3e991/coverage-7.13.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f9bada7bc660d20b23d7d312ebe29e927b655cf414dadcdb6335a2075695bd86", size = 252358, upload-time = "2026-02-03T14:01:02.824Z" },
+ { url = "https://files.pythonhosted.org/packages/36/8a/e69a5be51923097ba7d5cff9724466e74fe486e9232020ba97c809a8b42b/coverage-7.13.3-cp313-cp313-win32.whl", hash = "sha256:75b3c0300f3fa15809bd62d9ca8b170eb21fcf0100eb4b4154d6dc8b3a5bbd43", size = 222007, upload-time = "2026-02-03T14:01:04.876Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/09/a5a069bcee0d613bdd48ee7637fa73bc09e7ed4342b26890f2df97cc9682/coverage-7.13.3-cp313-cp313-win_amd64.whl", hash = "sha256:a2f7589c6132c44c53f6e705e1a6677e2b7821378c22f7703b2cf5388d0d4587", size = 222812, upload-time = "2026-02-03T14:01:07.296Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/4f/d62ad7dfe32f9e3d4a10c178bb6f98b10b083d6e0530ca202b399371f6c1/coverage-7.13.3-cp313-cp313-win_arm64.whl", hash = "sha256:123ceaf2b9d8c614f01110f908a341e05b1b305d6b2ada98763b9a5a59756051", size = 221433, upload-time = "2026-02-03T14:01:09.156Z" },
+ { url = "https://files.pythonhosted.org/packages/04/b2/4876c46d723d80b9c5b695f1a11bf5f7c3dabf540ec00d6edc076ff025e6/coverage-7.13.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:cc7fd0f726795420f3678ac82ff882c7fc33770bd0074463b5aef7293285ace9", size = 220162, upload-time = "2026-02-03T14:01:11.409Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/04/9942b64a0e0bdda2c109f56bda42b2a59d9d3df4c94b85a323c1cae9fc77/coverage-7.13.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d358dc408edc28730aed5477a69338e444e62fba0b7e9e4a131c505fadad691e", size = 220510, upload-time = "2026-02-03T14:01:13.038Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/82/5cfe1e81eae525b74669f9795f37eb3edd4679b873d79d1e6c1c14ee6c1c/coverage-7.13.3-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5d67b9ed6f7b5527b209b24b3df9f2e5bf0198c1bbf99c6971b0e2dcb7e2a107", size = 261801, upload-time = "2026-02-03T14:01:14.674Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/ec/a553d7f742fd2cd12e36a16a7b4b3582d5934b496ef2b5ea8abeb10903d4/coverage-7.13.3-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:59224bfb2e9b37c1335ae35d00daa3a5b4e0b1a20f530be208fff1ecfa436f43", size = 263882, upload-time = "2026-02-03T14:01:16.343Z" },
+ { url = "https://files.pythonhosted.org/packages/e1/58/8f54a2a93e3d675635bc406de1c9ac8d551312142ff52c9d71b5e533ad45/coverage-7.13.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ae9306b5299e31e31e0d3b908c66bcb6e7e3ddca143dea0266e9ce6c667346d3", size = 266306, upload-time = "2026-02-03T14:01:18.02Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/be/e593399fd6ea1f00aee79ebd7cc401021f218d34e96682a92e1bae092ff6/coverage-7.13.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:343aaeb5f8bb7bcd38620fd7bc56e6ee8207847d8c6103a1e7b72322d381ba4a", size = 261051, upload-time = "2026-02-03T14:01:19.757Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/e5/e9e0f6138b21bcdebccac36fbfde9cf15eb1bbcea9f5b1f35cd1f465fb91/coverage-7.13.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b2182129f4c101272ff5f2f18038d7b698db1bf8e7aa9e615cb48440899ad32e", size = 263868, upload-time = "2026-02-03T14:01:21.487Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/bf/de72cfebb69756f2d4a2dde35efcc33c47d85cd3ebdf844b3914aac2ef28/coverage-7.13.3-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:94d2ac94bd0cc57c5626f52f8c2fffed1444b5ae8c9fc68320306cc2b255e155", size = 261498, upload-time = "2026-02-03T14:01:23.097Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/91/4a2d313a70fc2e98ca53afd1c8ce67a89b1944cd996589a5b1fe7fbb3e5c/coverage-7.13.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:65436cde5ecabe26fb2f0bf598962f0a054d3f23ad529361326ac002c61a2a1e", size = 260394, upload-time = "2026-02-03T14:01:24.949Z" },
+ { url = "https://files.pythonhosted.org/packages/40/83/25113af7cf6941e779eb7ed8de2a677865b859a07ccee9146d4cc06a03e3/coverage-7.13.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:db83b77f97129813dbd463a67e5335adc6a6a91db652cc085d60c2d512746f96", size = 262579, upload-time = "2026-02-03T14:01:26.703Z" },
+ { url = "https://files.pythonhosted.org/packages/1e/19/a5f2b96262977e82fb9aabbe19b4d83561f5d063f18dde3e72f34ffc3b2f/coverage-7.13.3-cp313-cp313t-win32.whl", hash = "sha256:dfb428e41377e6b9ba1b0a32df6db5409cb089a0ed1d0a672dc4953ec110d84f", size = 222679, upload-time = "2026-02-03T14:01:28.553Z" },
+ { url = "https://files.pythonhosted.org/packages/81/82/ef1747b88c87a5c7d7edc3704799ebd650189a9158e680a063308b6125ef/coverage-7.13.3-cp313-cp313t-win_amd64.whl", hash = "sha256:5badd7e596e6b0c89aa8ec6d37f4473e4357f982ce57f9a2942b0221cd9cf60c", size = 223740, upload-time = "2026-02-03T14:01:30.776Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/4c/a67c7bb5b560241c22736a9cb2f14c5034149ffae18630323fde787339e4/coverage-7.13.3-cp313-cp313t-win_arm64.whl", hash = "sha256:989aa158c0eb19d83c76c26f4ba00dbb272485c56e452010a3450bdbc9daafd9", size = 221996, upload-time = "2026-02-03T14:01:32.495Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/b3/677bb43427fed9298905106f39c6520ac75f746f81b8f01104526a8026e4/coverage-7.13.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c6f6169bbdbdb85aab8ac0392d776948907267fcc91deeacf6f9d55f7a83ae3b", size = 219513, upload-time = "2026-02-03T14:01:34.29Z" },
+ { url = "https://files.pythonhosted.org/packages/42/53/290046e3bbf8986cdb7366a42dab3440b9983711eaff044a51b11006c67b/coverage-7.13.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2f5e731627a3d5ef11a2a35aa0c6f7c435867c7ccbc391268eb4f2ca5dbdcc10", size = 219850, upload-time = "2026-02-03T14:01:35.984Z" },
+ { url = "https://files.pythonhosted.org/packages/ea/2b/ab41f10345ba2e49d5e299be8663be2b7db33e77ac1b85cd0af985ea6406/coverage-7.13.3-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9db3a3285d91c0b70fab9f39f0a4aa37d375873677efe4e71e58d8321e8c5d39", size = 250886, upload-time = "2026-02-03T14:01:38.287Z" },
+ { url = "https://files.pythonhosted.org/packages/72/2d/b3f6913ee5a1d5cdd04106f257e5fac5d048992ffc2d9995d07b0f17739f/coverage-7.13.3-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:06e49c5897cb12e3f7ecdc111d44e97c4f6d0557b81a7a0204ed70a8b038f86f", size = 253393, upload-time = "2026-02-03T14:01:40.118Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/f6/b1f48810ffc6accf49a35b9943636560768f0812330f7456aa87dc39aff5/coverage-7.13.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fb25061a66802df9fc13a9ba1967d25faa4dae0418db469264fd9860a921dde4", size = 254740, upload-time = "2026-02-03T14:01:42.413Z" },
+ { url = "https://files.pythonhosted.org/packages/57/d0/e59c54f9be0b61808f6bc4c8c4346bd79f02dd6bbc3f476ef26124661f20/coverage-7.13.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:99fee45adbb1caeb914da16f70e557fb7ff6ddc9e4b14de665bd41af631367ef", size = 250905, upload-time = "2026-02-03T14:01:44.163Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/f7/5291bcdf498bafbee3796bb32ef6966e9915aebd4d0954123c8eae921c32/coverage-7.13.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:318002f1fd819bdc1651c619268aa5bc853c35fa5cc6d1e8c96bd9cd6c828b75", size = 252753, upload-time = "2026-02-03T14:01:45.974Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/a9/1dcafa918c281554dae6e10ece88c1add82db685be123e1b05c2056ff3fb/coverage-7.13.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:71295f2d1d170b9977dc386d46a7a1b7cbb30e5405492529b4c930113a33f895", size = 250716, upload-time = "2026-02-03T14:01:48.844Z" },
+ { url = "https://files.pythonhosted.org/packages/44/bb/4ea4eabcce8c4f6235df6e059fbc5db49107b24c4bdffc44aee81aeca5a8/coverage-7.13.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:5b1ad2e0dc672625c44bc4fe34514602a9fd8b10d52ddc414dc585f74453516c", size = 250530, upload-time = "2026-02-03T14:01:50.793Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/31/4a6c9e6a71367e6f923b27b528448c37f4e959b7e4029330523014691007/coverage-7.13.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b2beb64c145593a50d90db5c7178f55daeae129123b0d265bdb3cbec83e5194a", size = 252186, upload-time = "2026-02-03T14:01:52.607Z" },
+ { url = "https://files.pythonhosted.org/packages/27/92/e1451ef6390a4f655dc42da35d9971212f7abbbcad0bdb7af4407897eb76/coverage-7.13.3-cp314-cp314-win32.whl", hash = "sha256:3d1aed4f4e837a832df2f3b4f68a690eede0de4560a2dbc214ea0bc55aabcdb4", size = 222253, upload-time = "2026-02-03T14:01:55.071Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/98/78885a861a88de020c32a2693487c37d15a9873372953f0c3c159d575a43/coverage-7.13.3-cp314-cp314-win_amd64.whl", hash = "sha256:9f9efbbaf79f935d5fbe3ad814825cbce4f6cdb3054384cb49f0c0f496125fa0", size = 223069, upload-time = "2026-02-03T14:01:56.95Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/fb/3784753a48da58a5337972abf7ca58b1fb0f1bda21bc7b4fae992fd28e47/coverage-7.13.3-cp314-cp314-win_arm64.whl", hash = "sha256:31b6e889c53d4e6687ca63706148049494aace140cffece1c4dc6acadb70a7b3", size = 221633, upload-time = "2026-02-03T14:01:58.758Z" },
+ { url = "https://files.pythonhosted.org/packages/40/f9/75b732d9674d32cdbffe801ed5f770786dd1c97eecedef2125b0d25102dc/coverage-7.13.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:c5e9787cec750793a19a28df7edd85ac4e49d3fb91721afcdc3b86f6c08d9aa8", size = 220243, upload-time = "2026-02-03T14:02:01.109Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/7e/2868ec95de5a65703e6f0c87407ea822d1feb3619600fbc3c1c4fa986090/coverage-7.13.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:e5b86db331c682fd0e4be7098e6acee5e8a293f824d41487c667a93705d415ca", size = 220515, upload-time = "2026-02-03T14:02:02.862Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/eb/9f0d349652fced20bcaea0f67fc5777bd097c92369f267975732f3dc5f45/coverage-7.13.3-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:edc7754932682d52cf6e7a71806e529ecd5ce660e630e8bd1d37109a2e5f63ba", size = 261874, upload-time = "2026-02-03T14:02:04.727Z" },
+ { url = "https://files.pythonhosted.org/packages/ee/a5/6619bc4a6c7b139b16818149a3e74ab2e21599ff9a7b6811b6afde99f8ec/coverage-7.13.3-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d3a16d6398666510a6886f67f43d9537bfd0e13aca299688a19daa84f543122f", size = 264004, upload-time = "2026-02-03T14:02:06.634Z" },
+ { url = "https://files.pythonhosted.org/packages/29/b7/90aa3fc645a50c6f07881fca4fd0ba21e3bfb6ce3a7078424ea3a35c74c9/coverage-7.13.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:303d38b19626c1981e1bb067a9928236d88eb0e4479b18a74812f05a82071508", size = 266408, upload-time = "2026-02-03T14:02:09.037Z" },
+ { url = "https://files.pythonhosted.org/packages/62/55/08bb2a1e4dcbae384e638f0effef486ba5987b06700e481691891427d879/coverage-7.13.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:284e06eadfe15ddfee2f4ee56631f164ef897a7d7d5a15bca5f0bb88889fc5ba", size = 260977, upload-time = "2026-02-03T14:02:11.755Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/76/8bd4ae055a42d8fb5dd2230e5cf36ff2e05f85f2427e91b11a27fea52ed7/coverage-7.13.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d401f0864a1d3198422816878e4e84ca89ec1c1bf166ecc0ae01380a39b888cd", size = 263868, upload-time = "2026-02-03T14:02:13.565Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/f9/ba000560f11e9e32ec03df5aa8477242c2d95b379c99ac9a7b2e7fbacb1a/coverage-7.13.3-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:3f379b02c18a64de78c4ccdddf1c81c2c5ae1956c72dacb9133d7dd7809794ab", size = 261474, upload-time = "2026-02-03T14:02:16.069Z" },
+ { url = "https://files.pythonhosted.org/packages/90/4b/4de4de8f9ca7af4733bfcf4baa440121b7dbb3856daf8428ce91481ff63b/coverage-7.13.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:7a482f2da9086971efb12daca1d6547007ede3674ea06e16d7663414445c683e", size = 260317, upload-time = "2026-02-03T14:02:17.996Z" },
+ { url = "https://files.pythonhosted.org/packages/05/71/5cd8436e2c21410ff70be81f738c0dddea91bcc3189b1517d26e0102ccb3/coverage-7.13.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:562136b0d401992118d9b49fbee5454e16f95f85b120a4226a04d816e33fe024", size = 262635, upload-time = "2026-02-03T14:02:20.405Z" },
+ { url = "https://files.pythonhosted.org/packages/e7/f8/2834bb45bdd70b55a33ec354b8b5f6062fc90e5bb787e14385903a979503/coverage-7.13.3-cp314-cp314t-win32.whl", hash = "sha256:ca46e5c3be3b195098dd88711890b8011a9fa4feca942292bb84714ce5eab5d3", size = 223035, upload-time = "2026-02-03T14:02:22.323Z" },
+ { url = "https://files.pythonhosted.org/packages/26/75/f8290f0073c00d9ae14056d2b84ab92dff21d5370e464cb6cb06f52bf580/coverage-7.13.3-cp314-cp314t-win_amd64.whl", hash = "sha256:06d316dbb3d9fd44cca05b2dbcfbef22948493d63a1f28e828d43e6cc505fed8", size = 224142, upload-time = "2026-02-03T14:02:24.143Z" },
+ { url = "https://files.pythonhosted.org/packages/03/01/43ac78dfea8946c4a9161bbc034b5549115cb2b56781a4b574927f0d141a/coverage-7.13.3-cp314-cp314t-win_arm64.whl", hash = "sha256:299d66e9218193f9dc6e4880629ed7c4cd23486005166247c283fb98531656c3", size = 222166, upload-time = "2026-02-03T14:02:26.005Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/fb/70af542d2d938c778c9373ce253aa4116dbe7c0a5672f78b2b2ae0e1b94b/coverage-7.13.3-py3-none-any.whl", hash = "sha256:90a8af9dba6429b2573199622d72e0ebf024d6276f16abce394ad4d181bb0910", size = 211237, upload-time = "2026-02-03T14:02:27.986Z" },
+]
+
+[package.optional-dependencies]
+toml = [
+ { name = "tomli", marker = "python_full_version <= '3.11'" },
+]
+
+[[package]]
+name = "exceptiongroup"
+version = "1.3.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "typing-extensions", marker = "python_full_version < '3.13'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" },
+]
+
+[[package]]
+name = "flake8"
+version = "7.3.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "mccabe" },
+ { name = "pycodestyle" },
+ { name = "pyflakes" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/9b/af/fbfe3c4b5a657d79e5c47a2827a362f9e1b763336a52f926126aa6dc7123/flake8-7.3.0.tar.gz", hash = "sha256:fe044858146b9fc69b551a4b490d69cf960fcb78ad1edcb84e7fbb1b4a8e3872", size = 48326, upload-time = "2025-06-20T19:31:35.838Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/9f/56/13ab06b4f93ca7cac71078fbe37fcea175d3216f31f85c3168a6bbd0bb9a/flake8-7.3.0-py2.py3-none-any.whl", hash = "sha256:b9696257b9ce8beb888cdbe31cf885c90d31928fe202be0889a7cdafad32f01e", size = 57922, upload-time = "2025-06-20T19:31:34.425Z" },
+]
+
+[[package]]
+name = "iniconfig"
+version = "2.3.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
+]
+
+[[package]]
+name = "isort"
+version = "7.0.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/63/53/4f3c058e3bace40282876f9b553343376ee687f3c35a525dc79dbd450f88/isort-7.0.0.tar.gz", hash = "sha256:5513527951aadb3ac4292a41a16cbc50dd1642432f5e8c20057d414bdafb4187", size = 805049, upload-time = "2025-10-11T13:30:59.107Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7f/ed/e3705d6d02b4f7aea715a353c8ce193efd0b5db13e204df895d38734c244/isort-7.0.0-py3-none-any.whl", hash = "sha256:1bcabac8bc3c36c7fb7b98a76c8abb18e0f841a3ba81decac7691008592499c1", size = 94672, upload-time = "2025-10-11T13:30:57.665Z" },
+]
+
+[[package]]
+name = "librt"
+version = "0.7.8"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/e7/24/5f3646ff414285e0f7708fa4e946b9bf538345a41d1c375c439467721a5e/librt-0.7.8.tar.gz", hash = "sha256:1a4ede613941d9c3470b0368be851df6bb78ab218635512d0370b27a277a0862", size = 148323, upload-time = "2026-01-14T12:56:16.876Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/44/13/57b06758a13550c5f09563893b004f98e9537ee6ec67b7df85c3571c8832/librt-0.7.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b45306a1fc5f53c9330fbee134d8b3227fe5da2ab09813b892790400aa49352d", size = 56521, upload-time = "2026-01-14T12:54:40.066Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/24/bbea34d1452a10612fb45ac8356f95351ba40c2517e429602160a49d1fd0/librt-0.7.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:864c4b7083eeee250ed55135d2127b260d7eb4b5e953a9e5df09c852e327961b", size = 58456, upload-time = "2026-01-14T12:54:41.471Z" },
+ { url = "https://files.pythonhosted.org/packages/04/72/a168808f92253ec3a810beb1eceebc465701197dbc7e865a1c9ceb3c22c7/librt-0.7.8-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:6938cc2de153bc927ed8d71c7d2f2ae01b4e96359126c602721340eb7ce1a92d", size = 164392, upload-time = "2026-01-14T12:54:42.843Z" },
+ { url = "https://files.pythonhosted.org/packages/14/5c/4c0d406f1b02735c2e7af8ff1ff03a6577b1369b91aa934a9fa2cc42c7ce/librt-0.7.8-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:66daa6ac5de4288a5bbfbe55b4caa7bf0cd26b3269c7a476ffe8ce45f837f87d", size = 172959, upload-time = "2026-01-14T12:54:44.602Z" },
+ { url = "https://files.pythonhosted.org/packages/82/5f/3e85351c523f73ad8d938989e9a58c7f59fb9c17f761b9981b43f0025ce7/librt-0.7.8-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4864045f49dc9c974dadb942ac56a74cd0479a2aafa51ce272c490a82322ea3c", size = 186717, upload-time = "2026-01-14T12:54:45.986Z" },
+ { url = "https://files.pythonhosted.org/packages/08/f8/18bfe092e402d00fe00d33aa1e01dda1bd583ca100b393b4373847eade6d/librt-0.7.8-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a36515b1328dc5b3ffce79fe204985ca8572525452eacabee2166f44bb387b2c", size = 184585, upload-time = "2026-01-14T12:54:47.139Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/fc/f43972ff56fd790a9fa55028a52ccea1875100edbb856b705bd393b601e3/librt-0.7.8-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b7e7f140c5169798f90b80d6e607ed2ba5059784968a004107c88ad61fb3641d", size = 180497, upload-time = "2026-01-14T12:54:48.946Z" },
+ { url = "https://files.pythonhosted.org/packages/e1/3a/25e36030315a410d3ad0b7d0f19f5f188e88d1613d7d3fd8150523ea1093/librt-0.7.8-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ff71447cb778a4f772ddc4ce360e6ba9c95527ed84a52096bd1bbf9fee2ec7c0", size = 200052, upload-time = "2026-01-14T12:54:50.382Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/b8/f3a5a1931ae2a6ad92bf6893b9ef44325b88641d58723529e2c2935e8abe/librt-0.7.8-cp310-cp310-win32.whl", hash = "sha256:047164e5f68b7a8ebdf9fae91a3c2161d3192418aadd61ddd3a86a56cbe3dc85", size = 43477, upload-time = "2026-01-14T12:54:51.815Z" },
+ { url = "https://files.pythonhosted.org/packages/fe/91/c4202779366bc19f871b4ad25db10fcfa1e313c7893feb942f32668e8597/librt-0.7.8-cp310-cp310-win_amd64.whl", hash = "sha256:d6f254d096d84156a46a84861183c183d30734e52383602443292644d895047c", size = 49806, upload-time = "2026-01-14T12:54:53.149Z" },
+ { url = "https://files.pythonhosted.org/packages/1b/a3/87ea9c1049f2c781177496ebee29430e4631f439b8553a4969c88747d5d8/librt-0.7.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ff3e9c11aa260c31493d4b3197d1e28dd07768594a4f92bec4506849d736248f", size = 56507, upload-time = "2026-01-14T12:54:54.156Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/4a/23bcef149f37f771ad30203d561fcfd45b02bc54947b91f7a9ac34815747/librt-0.7.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ddb52499d0b3ed4aa88746aaf6f36a08314677d5c346234c3987ddc506404eac", size = 58455, upload-time = "2026-01-14T12:54:55.978Z" },
+ { url = "https://files.pythonhosted.org/packages/22/6e/46eb9b85c1b9761e0f42b6e6311e1cc544843ac897457062b9d5d0b21df4/librt-0.7.8-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:e9c0afebbe6ce177ae8edba0c7c4d626f2a0fc12c33bb993d163817c41a7a05c", size = 164956, upload-time = "2026-01-14T12:54:57.311Z" },
+ { url = "https://files.pythonhosted.org/packages/7a/3f/aa7c7f6829fb83989feb7ba9aa11c662b34b4bd4bd5b262f2876ba3db58d/librt-0.7.8-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:631599598e2c76ded400c0a8722dec09217c89ff64dc54b060f598ed68e7d2a8", size = 174364, upload-time = "2026-01-14T12:54:59.089Z" },
+ { url = "https://files.pythonhosted.org/packages/3f/2d/d57d154b40b11f2cb851c4df0d4c4456bacd9b1ccc4ecb593ddec56c1a8b/librt-0.7.8-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c1ba843ae20db09b9d5c80475376168feb2640ce91cd9906414f23cc267a1ff", size = 188034, upload-time = "2026-01-14T12:55:00.141Z" },
+ { url = "https://files.pythonhosted.org/packages/59/f9/36c4dad00925c16cd69d744b87f7001792691857d3b79187e7a673e812fb/librt-0.7.8-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b5b007bb22ea4b255d3ee39dfd06d12534de2fcc3438567d9f48cdaf67ae1ae3", size = 186295, upload-time = "2026-01-14T12:55:01.303Z" },
+ { url = "https://files.pythonhosted.org/packages/23/9b/8a9889d3df5efb67695a67785028ccd58e661c3018237b73ad081691d0cb/librt-0.7.8-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:dbd79caaf77a3f590cbe32dc2447f718772d6eea59656a7dcb9311161b10fa75", size = 181470, upload-time = "2026-01-14T12:55:02.492Z" },
+ { url = "https://files.pythonhosted.org/packages/43/64/54d6ef11afca01fef8af78c230726a9394759f2addfbf7afc5e3cc032a45/librt-0.7.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:87808a8d1e0bd62a01cafc41f0fd6818b5a5d0ca0d8a55326a81643cdda8f873", size = 201713, upload-time = "2026-01-14T12:55:03.919Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/29/73e7ed2991330b28919387656f54109139b49e19cd72902f466bd44415fd/librt-0.7.8-cp311-cp311-win32.whl", hash = "sha256:31724b93baa91512bd0a376e7cf0b59d8b631ee17923b1218a65456fa9bda2e7", size = 43803, upload-time = "2026-01-14T12:55:04.996Z" },
+ { url = "https://files.pythonhosted.org/packages/3f/de/66766ff48ed02b4d78deea30392ae200bcbd99ae61ba2418b49fd50a4831/librt-0.7.8-cp311-cp311-win_amd64.whl", hash = "sha256:978e8b5f13e52cf23a9e80f3286d7546baa70bc4ef35b51d97a709d0b28e537c", size = 50080, upload-time = "2026-01-14T12:55:06.489Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/e3/33450438ff3a8c581d4ed7f798a70b07c3206d298cf0b87d3806e72e3ed8/librt-0.7.8-cp311-cp311-win_arm64.whl", hash = "sha256:20e3946863d872f7cabf7f77c6c9d370b8b3d74333d3a32471c50d3a86c0a232", size = 43383, upload-time = "2026-01-14T12:55:07.49Z" },
+ { url = "https://files.pythonhosted.org/packages/56/04/79d8fcb43cae376c7adbab7b2b9f65e48432c9eced62ac96703bcc16e09b/librt-0.7.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9b6943885b2d49c48d0cff23b16be830ba46b0152d98f62de49e735c6e655a63", size = 57472, upload-time = "2026-01-14T12:55:08.528Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/ba/60b96e93043d3d659da91752689023a73981336446ae82078cddf706249e/librt-0.7.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:46ef1f4b9b6cc364b11eea0ecc0897314447a66029ee1e55859acb3dd8757c93", size = 58986, upload-time = "2026-01-14T12:55:09.466Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/26/5215e4cdcc26e7be7eee21955a7e13cbf1f6d7d7311461a6014544596fac/librt-0.7.8-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:907ad09cfab21e3c86e8f1f87858f7049d1097f77196959c033612f532b4e592", size = 168422, upload-time = "2026-01-14T12:55:10.499Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/84/e8d1bc86fa0159bfc24f3d798d92cafd3897e84c7fea7fe61b3220915d76/librt-0.7.8-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2991b6c3775383752b3ca0204842743256f3ad3deeb1d0adc227d56b78a9a850", size = 177478, upload-time = "2026-01-14T12:55:11.577Z" },
+ { url = "https://files.pythonhosted.org/packages/57/11/d0268c4b94717a18aa91df1100e767b010f87b7ae444dafaa5a2d80f33a6/librt-0.7.8-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:03679b9856932b8c8f674e87aa3c55ea11c9274301f76ae8dc4d281bda55cf62", size = 192439, upload-time = "2026-01-14T12:55:12.7Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/56/1e8e833b95fe684f80f8894ae4d8b7d36acc9203e60478fcae599120a975/librt-0.7.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3968762fec1b2ad34ce57458b6de25dbb4142713e9ca6279a0d352fa4e9f452b", size = 191483, upload-time = "2026-01-14T12:55:13.838Z" },
+ { url = "https://files.pythonhosted.org/packages/17/48/f11cf28a2cb6c31f282009e2208312aa84a5ee2732859f7856ee306176d5/librt-0.7.8-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:bb7a7807523a31f03061288cc4ffc065d684c39db7644c676b47d89553c0d714", size = 185376, upload-time = "2026-01-14T12:55:15.017Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/6a/d7c116c6da561b9155b184354a60a3d5cdbf08fc7f3678d09c95679d13d9/librt-0.7.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad64a14b1e56e702e19b24aae108f18ad1bf7777f3af5fcd39f87d0c5a814449", size = 206234, upload-time = "2026-01-14T12:55:16.571Z" },
+ { url = "https://files.pythonhosted.org/packages/61/de/1975200bb0285fc921c5981d9978ce6ce11ae6d797df815add94a5a848a3/librt-0.7.8-cp312-cp312-win32.whl", hash = "sha256:0241a6ed65e6666236ea78203a73d800dbed896cf12ae25d026d75dc1fcd1dac", size = 44057, upload-time = "2026-01-14T12:55:18.077Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/cd/724f2d0b3461426730d4877754b65d39f06a41ac9d0a92d5c6840f72b9ae/librt-0.7.8-cp312-cp312-win_amd64.whl", hash = "sha256:6db5faf064b5bab9675c32a873436b31e01d66ca6984c6f7f92621656033a708", size = 50293, upload-time = "2026-01-14T12:55:19.179Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/cf/7e899acd9ee5727ad8160fdcc9994954e79fab371c66535c60e13b968ffc/librt-0.7.8-cp312-cp312-win_arm64.whl", hash = "sha256:57175aa93f804d2c08d2edb7213e09276bd49097611aefc37e3fa38d1fb99ad0", size = 43574, upload-time = "2026-01-14T12:55:20.185Z" },
+ { url = "https://files.pythonhosted.org/packages/a1/fe/b1f9de2829cf7fc7649c1dcd202cfd873837c5cc2fc9e526b0e7f716c3d2/librt-0.7.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4c3995abbbb60b3c129490fa985dfe6cac11d88fc3c36eeb4fb1449efbbb04fc", size = 57500, upload-time = "2026-01-14T12:55:21.219Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/d4/4a60fbe2e53b825f5d9a77325071d61cd8af8506255067bf0c8527530745/librt-0.7.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:44e0c2cbc9bebd074cf2cdbe472ca185e824be4e74b1c63a8e934cea674bebf2", size = 59019, upload-time = "2026-01-14T12:55:22.256Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/37/61ff80341ba5159afa524445f2d984c30e2821f31f7c73cf166dcafa5564/librt-0.7.8-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:4d2f1e492cae964b3463a03dc77a7fe8742f7855d7258c7643f0ee32b6651dd3", size = 169015, upload-time = "2026-01-14T12:55:23.24Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/86/13d4f2d6a93f181ebf2fc953868826653ede494559da8268023fe567fca3/librt-0.7.8-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:451e7ffcef8f785831fdb791bd69211f47e95dc4c6ddff68e589058806f044c6", size = 178161, upload-time = "2026-01-14T12:55:24.826Z" },
+ { url = "https://files.pythonhosted.org/packages/88/26/e24ef01305954fc4d771f1f09f3dd682f9eb610e1bec188ffb719374d26e/librt-0.7.8-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3469e1af9f1380e093ae06bedcbdd11e407ac0b303a56bbe9afb1d6824d4982d", size = 193015, upload-time = "2026-01-14T12:55:26.04Z" },
+ { url = "https://files.pythonhosted.org/packages/88/a0/92b6bd060e720d7a31ed474d046a69bd55334ec05e9c446d228c4b806ae3/librt-0.7.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f11b300027ce19a34f6d24ebb0a25fd0e24a9d53353225a5c1e6cadbf2916b2e", size = 192038, upload-time = "2026-01-14T12:55:27.208Z" },
+ { url = "https://files.pythonhosted.org/packages/06/bb/6f4c650253704279c3a214dad188101d1b5ea23be0606628bc6739456624/librt-0.7.8-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4adc73614f0d3c97874f02f2c7fd2a27854e7e24ad532ea6b965459c5b757eca", size = 186006, upload-time = "2026-01-14T12:55:28.594Z" },
+ { url = "https://files.pythonhosted.org/packages/dc/00/1c409618248d43240cadf45f3efb866837fa77e9a12a71481912135eb481/librt-0.7.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:60c299e555f87e4c01b2eca085dfccda1dde87f5a604bb45c2906b8305819a93", size = 206888, upload-time = "2026-01-14T12:55:30.214Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/83/b2cfe8e76ff5c1c77f8a53da3d5de62d04b5ebf7cf913e37f8bca43b5d07/librt-0.7.8-cp313-cp313-win32.whl", hash = "sha256:b09c52ed43a461994716082ee7d87618096851319bf695d57ec123f2ab708951", size = 44126, upload-time = "2026-01-14T12:55:31.44Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/0b/c59d45de56a51bd2d3a401fc63449c0ac163e4ef7f523ea8b0c0dee86ec5/librt-0.7.8-cp313-cp313-win_amd64.whl", hash = "sha256:f8f4a901a3fa28969d6e4519deceab56c55a09d691ea7b12ca830e2fa3461e34", size = 50262, upload-time = "2026-01-14T12:55:33.01Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/b9/973455cec0a1ec592395250c474164c4a58ebf3e0651ee920fef1a2623f1/librt-0.7.8-cp313-cp313-win_arm64.whl", hash = "sha256:43d4e71b50763fcdcf64725ac680d8cfa1706c928b844794a7aa0fa9ac8e5f09", size = 43600, upload-time = "2026-01-14T12:55:34.054Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/73/fa8814c6ce2d49c3827829cadaa1589b0bf4391660bd4510899393a23ebc/librt-0.7.8-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:be927c3c94c74b05128089a955fba86501c3b544d1d300282cc1b4bd370cb418", size = 57049, upload-time = "2026-01-14T12:55:35.056Z" },
+ { url = "https://files.pythonhosted.org/packages/53/fe/f6c70956da23ea235fd2e3cc16f4f0b4ebdfd72252b02d1164dd58b4e6c3/librt-0.7.8-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:7b0803e9008c62a7ef79058233db7ff6f37a9933b8f2573c05b07ddafa226611", size = 58689, upload-time = "2026-01-14T12:55:36.078Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/4d/7a2481444ac5fba63050d9abe823e6bc16896f575bfc9c1e5068d516cdce/librt-0.7.8-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:79feb4d00b2a4e0e05c9c56df707934f41fcb5fe53fd9efb7549068d0495b758", size = 166808, upload-time = "2026-01-14T12:55:37.595Z" },
+ { url = "https://files.pythonhosted.org/packages/ac/3c/10901d9e18639f8953f57c8986796cfbf4c1c514844a41c9197cf87cb707/librt-0.7.8-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b9122094e3f24aa759c38f46bd8863433820654927370250f460ae75488b66ea", size = 175614, upload-time = "2026-01-14T12:55:38.756Z" },
+ { url = "https://files.pythonhosted.org/packages/db/01/5cbdde0951a5090a80e5ba44e6357d375048123c572a23eecfb9326993a7/librt-0.7.8-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7e03bea66af33c95ce3addf87a9bf1fcad8d33e757bc479957ddbc0e4f7207ac", size = 189955, upload-time = "2026-01-14T12:55:39.939Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/b4/e80528d2f4b7eaf1d437fcbd6fc6ba4cbeb3e2a0cb9ed5a79f47c7318706/librt-0.7.8-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f1ade7f31675db00b514b98f9ab9a7698c7282dad4be7492589109471852d398", size = 189370, upload-time = "2026-01-14T12:55:41.057Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/ab/938368f8ce31a9787ecd4becb1e795954782e4312095daf8fd22420227c8/librt-0.7.8-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:a14229ac62adcf1b90a15992f1ab9c69ae8b99ffb23cb64a90878a6e8a2f5b81", size = 183224, upload-time = "2026-01-14T12:55:42.328Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/10/559c310e7a6e4014ac44867d359ef8238465fb499e7eb31b6bfe3e3f86f5/librt-0.7.8-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5bcaaf624fd24e6a0cb14beac37677f90793a96864c67c064a91458611446e83", size = 203541, upload-time = "2026-01-14T12:55:43.501Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/db/a0db7acdb6290c215f343835c6efda5b491bb05c3ddc675af558f50fdba3/librt-0.7.8-cp314-cp314-win32.whl", hash = "sha256:7aa7d5457b6c542ecaed79cec4ad98534373c9757383973e638ccced0f11f46d", size = 40657, upload-time = "2026-01-14T12:55:44.668Z" },
+ { url = "https://files.pythonhosted.org/packages/72/e0/4f9bdc2a98a798511e81edcd6b54fe82767a715e05d1921115ac70717f6f/librt-0.7.8-cp314-cp314-win_amd64.whl", hash = "sha256:3d1322800771bee4a91f3b4bd4e49abc7d35e65166821086e5afd1e6c0d9be44", size = 46835, upload-time = "2026-01-14T12:55:45.655Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/3d/59c6402e3dec2719655a41ad027a7371f8e2334aa794ed11533ad5f34969/librt-0.7.8-cp314-cp314-win_arm64.whl", hash = "sha256:5363427bc6a8c3b1719f8f3845ea53553d301382928a86e8fab7984426949bce", size = 39885, upload-time = "2026-01-14T12:55:47.138Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/9c/2481d80950b83085fb14ba3c595db56330d21bbc7d88a19f20165f3538db/librt-0.7.8-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:ca916919793a77e4a98d4a1701e345d337ce53be4a16620f063191f7322ac80f", size = 59161, upload-time = "2026-01-14T12:55:48.45Z" },
+ { url = "https://files.pythonhosted.org/packages/96/79/108df2cfc4e672336765d54e3ff887294c1cc36ea4335c73588875775527/librt-0.7.8-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:54feb7b4f2f6706bb82325e836a01be805770443e2400f706e824e91f6441dde", size = 61008, upload-time = "2026-01-14T12:55:49.527Z" },
+ { url = "https://files.pythonhosted.org/packages/46/f2/30179898f9994a5637459d6e169b6abdc982012c0a4b2d4c26f50c06f911/librt-0.7.8-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:39a4c76fee41007070f872b648cc2f711f9abf9a13d0c7162478043377b52c8e", size = 187199, upload-time = "2026-01-14T12:55:50.587Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/da/f7563db55cebdc884f518ba3791ad033becc25ff68eb70902b1747dc0d70/librt-0.7.8-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ac9c8a458245c7de80bc1b9765b177055efff5803f08e548dd4bb9ab9a8d789b", size = 198317, upload-time = "2026-01-14T12:55:51.991Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/6c/4289acf076ad371471fa86718c30ae353e690d3de6167f7db36f429272f1/librt-0.7.8-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:95b67aa7eff150f075fda09d11f6bfb26edffd300f6ab1666759547581e8f666", size = 210334, upload-time = "2026-01-14T12:55:53.682Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/7f/377521ac25b78ac0a5ff44127a0360ee6d5ddd3ce7327949876a30533daa/librt-0.7.8-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:535929b6eff670c593c34ff435d5440c3096f20fa72d63444608a5aef64dd581", size = 211031, upload-time = "2026-01-14T12:55:54.827Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/b1/e1e96c3e20b23d00cf90f4aad48f0deb4cdfec2f0ed8380d0d85acf98bbf/librt-0.7.8-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:63937bd0f4d1cb56653dc7ae900d6c52c41f0015e25aaf9902481ee79943b33a", size = 204581, upload-time = "2026-01-14T12:55:56.811Z" },
+ { url = "https://files.pythonhosted.org/packages/43/71/0f5d010e92ed9747e14bef35e91b6580533510f1e36a8a09eb79ee70b2f0/librt-0.7.8-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cf243da9e42d914036fd362ac3fa77d80a41cadcd11ad789b1b5eec4daaf67ca", size = 224731, upload-time = "2026-01-14T12:55:58.175Z" },
+ { url = "https://files.pythonhosted.org/packages/22/f0/07fb6ab5c39a4ca9af3e37554f9d42f25c464829254d72e4ebbd81da351c/librt-0.7.8-cp314-cp314t-win32.whl", hash = "sha256:171ca3a0a06c643bd0a2f62a8944e1902c94aa8e5da4db1ea9a8daf872685365", size = 41173, upload-time = "2026-01-14T12:55:59.315Z" },
+ { url = "https://files.pythonhosted.org/packages/24/d4/7e4be20993dc6a782639625bd2f97f3c66125c7aa80c82426956811cfccf/librt-0.7.8-cp314-cp314t-win_amd64.whl", hash = "sha256:445b7304145e24c60288a2f172b5ce2ca35c0f81605f5299f3fa567e189d2e32", size = 47668, upload-time = "2026-01-14T12:56:00.261Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/85/69f92b2a7b3c0f88ffe107c86b952b397004b5b8ea5a81da3d9c04c04422/librt-0.7.8-cp314-cp314t-win_arm64.whl", hash = "sha256:8766ece9de08527deabcd7cb1b4f1a967a385d26e33e536d6d8913db6ef74f06", size = 40550, upload-time = "2026-01-14T12:56:01.542Z" },
+]
+
+[[package]]
+name = "mccabe"
+version = "0.7.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/e7/ff/0ffefdcac38932a54d2b5eed4e0ba8a408f215002cd178ad1df0f2806ff8/mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", size = 9658, upload-time = "2022-01-24T01:14:51.113Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/27/1a/1f68f9ba0c207934b35b86a8ca3aad8395a3d6dd7921c0686e23853ff5a9/mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e", size = 7350, upload-time = "2022-01-24T01:14:49.62Z" },
+]
+
+[[package]]
+name = "mypy"
+version = "1.19.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "librt", marker = "platform_python_implementation != 'PyPy'" },
+ { name = "mypy-extensions" },
+ { name = "pathspec" },
+ { name = "tomli", marker = "python_full_version < '3.11'" },
+ { name = "typing-extensions" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/f5/db/4efed9504bc01309ab9c2da7e352cc223569f05478012b5d9ece38fd44d2/mypy-1.19.1.tar.gz", hash = "sha256:19d88bb05303fe63f71dd2c6270daca27cb9401c4ca8255fe50d1d920e0eb9ba", size = 3582404, upload-time = "2025-12-15T05:03:48.42Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/2f/63/e499890d8e39b1ff2df4c0c6ce5d371b6844ee22b8250687a99fd2f657a8/mypy-1.19.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5f05aa3d375b385734388e844bc01733bd33c644ab48e9684faa54e5389775ec", size = 13101333, upload-time = "2025-12-15T05:03:03.28Z" },
+ { url = "https://files.pythonhosted.org/packages/72/4b/095626fc136fba96effc4fd4a82b41d688ab92124f8c4f7564bffe5cf1b0/mypy-1.19.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:022ea7279374af1a5d78dfcab853fe6a536eebfda4b59deab53cd21f6cd9f00b", size = 12164102, upload-time = "2025-12-15T05:02:33.611Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/5b/952928dd081bf88a83a5ccd49aaecfcd18fd0d2710c7ff07b8fb6f7032b9/mypy-1.19.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee4c11e460685c3e0c64a4c5de82ae143622410950d6be863303a1c4ba0e36d6", size = 12765799, upload-time = "2025-12-15T05:03:28.44Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/0d/93c2e4a287f74ef11a66fb6d49c7a9f05e47b0a4399040e6719b57f500d2/mypy-1.19.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de759aafbae8763283b2ee5869c7255391fbc4de3ff171f8f030b5ec48381b74", size = 13522149, upload-time = "2025-12-15T05:02:36.011Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/0e/33a294b56aaad2b338d203e3a1d8b453637ac36cb278b45005e0901cf148/mypy-1.19.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ab43590f9cd5108f41aacf9fca31841142c786827a74ab7cc8a2eacb634e09a1", size = 13810105, upload-time = "2025-12-15T05:02:40.327Z" },
+ { url = "https://files.pythonhosted.org/packages/0e/fd/3e82603a0cb66b67c5e7abababce6bf1a929ddf67bf445e652684af5c5a0/mypy-1.19.1-cp310-cp310-win_amd64.whl", hash = "sha256:2899753e2f61e571b3971747e302d5f420c3fd09650e1951e99f823bc3089dac", size = 10057200, upload-time = "2025-12-15T05:02:51.012Z" },
+ { url = "https://files.pythonhosted.org/packages/ef/47/6b3ebabd5474d9cdc170d1342fbf9dddc1b0ec13ec90bf9004ee6f391c31/mypy-1.19.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d8dfc6ab58ca7dda47d9237349157500468e404b17213d44fc1cb77bce532288", size = 13028539, upload-time = "2025-12-15T05:03:44.129Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/a6/ac7c7a88a3c9c54334f53a941b765e6ec6c4ebd65d3fe8cdcfbe0d0fd7db/mypy-1.19.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e3f276d8493c3c97930e354b2595a44a21348b320d859fb4a2b9f66da9ed27ab", size = 12083163, upload-time = "2025-12-15T05:03:37.679Z" },
+ { url = "https://files.pythonhosted.org/packages/67/af/3afa9cf880aa4a2c803798ac24f1d11ef72a0c8079689fac5cfd815e2830/mypy-1.19.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2abb24cf3f17864770d18d673c85235ba52456b36a06b6afc1e07c1fdcd3d0e6", size = 12687629, upload-time = "2025-12-15T05:02:31.526Z" },
+ { url = "https://files.pythonhosted.org/packages/2d/46/20f8a7114a56484ab268b0ab372461cb3a8f7deed31ea96b83a4e4cfcfca/mypy-1.19.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a009ffa5a621762d0c926a078c2d639104becab69e79538a494bcccb62cc0331", size = 13436933, upload-time = "2025-12-15T05:03:15.606Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/f8/33b291ea85050a21f15da910002460f1f445f8007adb29230f0adea279cb/mypy-1.19.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f7cee03c9a2e2ee26ec07479f38ea9c884e301d42c6d43a19d20fb014e3ba925", size = 13661754, upload-time = "2025-12-15T05:02:26.731Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/a3/47cbd4e85bec4335a9cd80cf67dbc02be21b5d4c9c23ad6b95d6c5196bac/mypy-1.19.1-cp311-cp311-win_amd64.whl", hash = "sha256:4b84a7a18f41e167f7995200a1d07a4a6810e89d29859df936f1c3923d263042", size = 10055772, upload-time = "2025-12-15T05:03:26.179Z" },
+ { url = "https://files.pythonhosted.org/packages/06/8a/19bfae96f6615aa8a0604915512e0289b1fad33d5909bf7244f02935d33a/mypy-1.19.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a8174a03289288c1f6c46d55cef02379b478bfbc8e358e02047487cad44c6ca1", size = 13206053, upload-time = "2025-12-15T05:03:46.622Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/34/3e63879ab041602154ba2a9f99817bb0c85c4df19a23a1443c8986e4d565/mypy-1.19.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffcebe56eb09ff0c0885e750036a095e23793ba6c2e894e7e63f6d89ad51f22e", size = 12219134, upload-time = "2025-12-15T05:03:24.367Z" },
+ { url = "https://files.pythonhosted.org/packages/89/cc/2db6f0e95366b630364e09845672dbee0cbf0bbe753a204b29a944967cd9/mypy-1.19.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b64d987153888790bcdb03a6473d321820597ab8dd9243b27a92153c4fa50fd2", size = 12731616, upload-time = "2025-12-15T05:02:44.725Z" },
+ { url = "https://files.pythonhosted.org/packages/00/be/dd56c1fd4807bc1eba1cf18b2a850d0de7bacb55e158755eb79f77c41f8e/mypy-1.19.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c35d298c2c4bba75feb2195655dfea8124d855dfd7343bf8b8c055421eaf0cf8", size = 13620847, upload-time = "2025-12-15T05:03:39.633Z" },
+ { url = "https://files.pythonhosted.org/packages/6d/42/332951aae42b79329f743bf1da088cd75d8d4d9acc18fbcbd84f26c1af4e/mypy-1.19.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:34c81968774648ab5ac09c29a375fdede03ba253f8f8287847bd480782f73a6a", size = 13834976, upload-time = "2025-12-15T05:03:08.786Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/63/e7493e5f90e1e085c562bb06e2eb32cae27c5057b9653348d38b47daaecc/mypy-1.19.1-cp312-cp312-win_amd64.whl", hash = "sha256:b10e7c2cd7870ba4ad9b2d8a6102eb5ffc1f16ca35e3de6bfa390c1113029d13", size = 10118104, upload-time = "2025-12-15T05:03:10.834Z" },
+ { url = "https://files.pythonhosted.org/packages/de/9f/a6abae693f7a0c697dbb435aac52e958dc8da44e92e08ba88d2e42326176/mypy-1.19.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e3157c7594ff2ef1634ee058aafc56a82db665c9438fd41b390f3bde1ab12250", size = 13201927, upload-time = "2025-12-15T05:02:29.138Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/a4/45c35ccf6e1c65afc23a069f50e2c66f46bd3798cbe0d680c12d12935caa/mypy-1.19.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bdb12f69bcc02700c2b47e070238f42cb87f18c0bc1fc4cdb4fb2bc5fd7a3b8b", size = 12206730, upload-time = "2025-12-15T05:03:01.325Z" },
+ { url = "https://files.pythonhosted.org/packages/05/bb/cdcf89678e26b187650512620eec8368fded4cfd99cfcb431e4cdfd19dec/mypy-1.19.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f859fb09d9583a985be9a493d5cfc5515b56b08f7447759a0c5deaf68d80506e", size = 12724581, upload-time = "2025-12-15T05:03:20.087Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/32/dd260d52babf67bad8e6770f8e1102021877ce0edea106e72df5626bb0ec/mypy-1.19.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c9a6538e0415310aad77cb94004ca6482330fece18036b5f360b62c45814c4ef", size = 13616252, upload-time = "2025-12-15T05:02:49.036Z" },
+ { url = "https://files.pythonhosted.org/packages/71/d0/5e60a9d2e3bd48432ae2b454b7ef2b62a960ab51292b1eda2a95edd78198/mypy-1.19.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:da4869fc5e7f62a88f3fe0b5c919d1d9f7ea3cef92d3689de2823fd27e40aa75", size = 13840848, upload-time = "2025-12-15T05:02:55.95Z" },
+ { url = "https://files.pythonhosted.org/packages/98/76/d32051fa65ecf6cc8c6610956473abdc9b4c43301107476ac03559507843/mypy-1.19.1-cp313-cp313-win_amd64.whl", hash = "sha256:016f2246209095e8eda7538944daa1d60e1e8134d98983b9fc1e92c1fc0cb8dd", size = 10135510, upload-time = "2025-12-15T05:02:58.438Z" },
+ { url = "https://files.pythonhosted.org/packages/de/eb/b83e75f4c820c4247a58580ef86fcd35165028f191e7e1ba57128c52782d/mypy-1.19.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06e6170bd5836770e8104c8fdd58e5e725cfeb309f0a6c681a811f557e97eac1", size = 13199744, upload-time = "2025-12-15T05:03:30.823Z" },
+ { url = "https://files.pythonhosted.org/packages/94/28/52785ab7bfa165f87fcbb61547a93f98bb20e7f82f90f165a1f69bce7b3d/mypy-1.19.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:804bd67b8054a85447c8954215a906d6eff9cabeabe493fb6334b24f4bfff718", size = 12215815, upload-time = "2025-12-15T05:02:42.323Z" },
+ { url = "https://files.pythonhosted.org/packages/0a/c6/bdd60774a0dbfb05122e3e925f2e9e846c009e479dcec4821dad881f5b52/mypy-1.19.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:21761006a7f497cb0d4de3d8ef4ca70532256688b0523eee02baf9eec895e27b", size = 12740047, upload-time = "2025-12-15T05:03:33.168Z" },
+ { url = "https://files.pythonhosted.org/packages/32/2a/66ba933fe6c76bd40d1fe916a83f04fed253152f451a877520b3c4a5e41e/mypy-1.19.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28902ee51f12e0f19e1e16fbe2f8f06b6637f482c459dd393efddd0ec7f82045", size = 13601998, upload-time = "2025-12-15T05:03:13.056Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/da/5055c63e377c5c2418760411fd6a63ee2b96cf95397259038756c042574f/mypy-1.19.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:481daf36a4c443332e2ae9c137dfee878fcea781a2e3f895d54bd3002a900957", size = 13807476, upload-time = "2025-12-15T05:03:17.977Z" },
+ { url = "https://files.pythonhosted.org/packages/cd/09/4ebd873390a063176f06b0dbf1f7783dd87bd120eae7727fa4ae4179b685/mypy-1.19.1-cp314-cp314-win_amd64.whl", hash = "sha256:8bb5c6f6d043655e055be9b542aa5f3bdd30e4f3589163e85f93f3640060509f", size = 10281872, upload-time = "2025-12-15T05:03:05.549Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/f4/4ce9a05ce5ded1de3ec1c1d96cf9f9504a04e54ce0ed55cfa38619a32b8d/mypy-1.19.1-py3-none-any.whl", hash = "sha256:f1235f5ea01b7db5468d53ece6aaddf1ad0b88d9e7462b86ef96fe04995d7247", size = 2471239, upload-time = "2025-12-15T05:03:07.248Z" },
+]
+
+[[package]]
+name = "mypy-extensions"
+version = "1.1.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" },
+]
+
+[[package]]
+name = "packaging"
+version = "26.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/65/ee/299d360cdc32edc7d2cf530f3accf79c4fca01e96ffc950d8a52213bd8e4/packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4", size = 143416, upload-time = "2026-01-21T20:50:39.064Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" },
+]
+
+[[package]]
+name = "pathspec"
+version = "1.0.4"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/fa/36/e27608899f9b8d4dff0617b2d9ab17ca5608956ca44461ac14ac48b44015/pathspec-1.0.4.tar.gz", hash = "sha256:0210e2ae8a21a9137c0d470578cb0e595af87edaa6ebf12ff176f14a02e0e645", size = 131200, upload-time = "2026-01-27T03:59:46.938Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ef/3c/2c197d226f9ea224a9ab8d197933f9da0ae0aac5b6e0f884e2b8d9c8e9f7/pathspec-1.0.4-py3-none-any.whl", hash = "sha256:fb6ae2fd4e7c921a165808a552060e722767cfa526f99ca5156ed2ce45a5c723", size = 55206, upload-time = "2026-01-27T03:59:45.137Z" },
+]
+
+[[package]]
+name = "platformdirs"
+version = "4.5.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/cf/86/0248f086a84f01b37aaec0fa567b397df1a119f73c16f6c7a9aac73ea309/platformdirs-4.5.1.tar.gz", hash = "sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda", size = 21715, upload-time = "2025-12-05T13:52:58.638Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/cb/28/3bfe2fa5a7b9c46fe7e13c97bda14c895fb10fa2ebf1d0abb90e0cea7ee1/platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31", size = 18731, upload-time = "2025-12-05T13:52:56.823Z" },
+]
+
+[[package]]
+name = "pluggy"
+version = "1.6.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
+]
+
+[[package]]
+name = "pyaes"
+version = "1.6.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/44/66/2c17bae31c906613795711fc78045c285048168919ace2220daa372c7d72/pyaes-1.6.1.tar.gz", hash = "sha256:02c1b1405c38d3c370b085fb952dd8bea3fadcee6411ad99f312cc129c536d8f", size = 28536, upload-time = "2017-09-20T21:17:54.23Z" }
+
+[[package]]
+name = "pyasn1"
+version = "0.6.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/fe/b6/6e630dff89739fcd427e3f72b3d905ce0acb85a45d4ec3e2678718a3487f/pyasn1-0.6.2.tar.gz", hash = "sha256:9b59a2b25ba7e4f8197db7686c09fb33e658b98339fadb826e9512629017833b", size = 146586, upload-time = "2026-01-16T18:04:18.534Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/44/b5/a96872e5184f354da9c84ae119971a0a4c221fe9b27a4d94bd43f2596727/pyasn1-0.6.2-py3-none-any.whl", hash = "sha256:1eb26d860996a18e9b6ed05e7aae0e9fc21619fcee6af91cca9bad4fbea224bf", size = 83371, upload-time = "2026-01-16T18:04:17.174Z" },
+]
+
+[[package]]
+name = "pycodestyle"
+version = "2.14.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/11/e0/abfd2a0d2efe47670df87f3e3a0e2edda42f055053c85361f19c0e2c1ca8/pycodestyle-2.14.0.tar.gz", hash = "sha256:c4b5b517d278089ff9d0abdec919cd97262a3367449ea1c8b49b91529167b783", size = 39472, upload-time = "2025-06-20T18:49:48.75Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d7/27/a58ddaf8c588a3ef080db9d0b7e0b97215cee3a45df74f3a94dbbf5c893a/pycodestyle-2.14.0-py2.py3-none-any.whl", hash = "sha256:dd6bf7cb4ee77f8e016f9c8e74a35ddd9f67e1d5fd4184d86c3b98e07099f42d", size = 31594, upload-time = "2025-06-20T18:49:47.491Z" },
+]
+
+[[package]]
+name = "pyflakes"
+version = "3.4.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/45/dc/fd034dc20b4b264b3d015808458391acbf9df40b1e54750ef175d39180b1/pyflakes-3.4.0.tar.gz", hash = "sha256:b24f96fafb7d2ab0ec5075b7350b3d2d2218eab42003821c06344973d3ea2f58", size = 64669, upload-time = "2025-06-20T18:45:27.834Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c2/2f/81d580a0fb83baeb066698975cb14a618bdbed7720678566f1b046a95fe8/pyflakes-3.4.0-py2.py3-none-any.whl", hash = "sha256:f742a7dbd0d9cb9ea41e9a24a918996e8170c799fa528688d40dd582c8265f4f", size = 63551, upload-time = "2025-06-20T18:45:26.937Z" },
+]
+
+[[package]]
+name = "pygments"
+version = "2.19.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/b0/77/a5b8c569bf593b0140bde72ea885a803b82086995367bf2037de0159d924/pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887", size = 4968631, upload-time = "2025-06-21T13:39:12.283Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
+]
+
+[[package]]
+name = "pytest"
+version = "9.0.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "colorama", marker = "sys_platform == 'win32'" },
+ { name = "exceptiongroup", marker = "python_full_version < '3.11'" },
+ { name = "iniconfig" },
+ { name = "packaging" },
+ { name = "pluggy" },
+ { name = "pygments" },
+ { name = "tomli", marker = "python_full_version < '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" },
+]
+
+[[package]]
+name = "pytest-asyncio"
+version = "1.3.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "backports-asyncio-runner", marker = "python_full_version < '3.11'" },
+ { name = "pytest" },
+ { name = "typing-extensions", marker = "python_full_version < '3.13'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/90/2c/8af215c0f776415f3590cac4f9086ccefd6fd463befeae41cd4d3f193e5a/pytest_asyncio-1.3.0.tar.gz", hash = "sha256:d7f52f36d231b80ee124cd216ffb19369aa168fc10095013c6b014a34d3ee9e5", size = 50087, upload-time = "2025-11-10T16:07:47.256Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5", size = 15075, upload-time = "2025-11-10T16:07:45.537Z" },
+]
+
+[[package]]
+name = "pytest-cov"
+version = "7.0.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "coverage", extra = ["toml"] },
+ { name = "pluggy" },
+ { name = "pytest" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/5e/f7/c933acc76f5208b3b00089573cf6a2bc26dc80a8aece8f52bb7d6b1855ca/pytest_cov-7.0.0.tar.gz", hash = "sha256:33c97eda2e049a0c5298e91f519302a1334c26ac65c1a483d6206fd458361af1", size = 54328, upload-time = "2025-09-09T10:57:02.113Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ee/49/1377b49de7d0c1ce41292161ea0f721913fa8722c19fb9c1e3aa0367eecb/pytest_cov-7.0.0-py3-none-any.whl", hash = "sha256:3b8e9558b16cc1479da72058bdecf8073661c7f57f7d3c5f22a1c23507f2d861", size = 22424, upload-time = "2025-09-09T10:57:00.695Z" },
+]
+
+[[package]]
+name = "python-dotenv"
+version = "1.2.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f0/26/19cadc79a718c5edbec86fd4919a6b6d3f681039a2f6d66d14be94e75fb9/python_dotenv-1.2.1.tar.gz", hash = "sha256:42667e897e16ab0d66954af0e60a9caa94f0fd4ecf3aaf6d2d260eec1aa36ad6", size = 44221, upload-time = "2025-10-26T15:12:10.434Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/14/1b/a298b06749107c305e1fe0f814c6c74aea7b2f1e10989cb30f544a1b3253/python_dotenv-1.2.1-py3-none-any.whl", hash = "sha256:b81ee9561e9ca4004139c6cbba3a238c32b03e4894671e181b671e8cb8425d61", size = 21230, upload-time = "2025-10-26T15:12:09.109Z" },
+]
+
+[[package]]
+name = "pytokens"
+version = "0.4.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/b6/34/b4e015b99031667a7b960f888889c5bd34ef585c85e1cb56a594b92836ac/pytokens-0.4.1.tar.gz", hash = "sha256:292052fe80923aae2260c073f822ceba21f3872ced9a68bb7953b348e561179a", size = 23015, upload-time = "2026-01-30T01:03:45.924Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/42/24/f206113e05cb8ef51b3850e7ef88f20da6f4bf932190ceb48bd3da103e10/pytokens-0.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2a44ed93ea23415c54f3face3b65ef2b844d96aeb3455b8a69b3df6beab6acc5", size = 161522, upload-time = "2026-01-30T01:02:50.393Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/e9/06a6bf1b90c2ed81a9c7d2544232fe5d2891d1cd480e8a1809ca354a8eb2/pytokens-0.4.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:add8bf86b71a5d9fb5b89f023a80b791e04fba57960aa790cc6125f7f1d39dfe", size = 246945, upload-time = "2026-01-30T01:02:52.399Z" },
+ { url = "https://files.pythonhosted.org/packages/69/66/f6fb1007a4c3d8b682d5d65b7c1fb33257587a5f782647091e3408abe0b8/pytokens-0.4.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:670d286910b531c7b7e3c0b453fd8156f250adb140146d234a82219459b9640c", size = 259525, upload-time = "2026-01-30T01:02:53.737Z" },
+ { url = "https://files.pythonhosted.org/packages/04/92/086f89b4d622a18418bac74ab5db7f68cf0c21cf7cc92de6c7b919d76c88/pytokens-0.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:4e691d7f5186bd2842c14813f79f8884bb03f5995f0575272009982c5ac6c0f7", size = 262693, upload-time = "2026-01-30T01:02:54.871Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/7b/8b31c347cf94a3f900bdde750b2e9131575a61fdb620d3d3c75832262137/pytokens-0.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:27b83ad28825978742beef057bfe406ad6ed524b2d28c252c5de7b4a6dd48fa2", size = 103567, upload-time = "2026-01-30T01:02:56.414Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/92/790ebe03f07b57e53b10884c329b9a1a308648fc083a6d4a39a10a28c8fc/pytokens-0.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d70e77c55ae8380c91c0c18dea05951482e263982911fc7410b1ffd1dadd3440", size = 160864, upload-time = "2026-01-30T01:02:57.882Z" },
+ { url = "https://files.pythonhosted.org/packages/13/25/a4f555281d975bfdd1eba731450e2fe3a95870274da73fb12c40aeae7625/pytokens-0.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4a58d057208cb9075c144950d789511220b07636dd2e4708d5645d24de666bdc", size = 248565, upload-time = "2026-01-30T01:02:59.912Z" },
+ { url = "https://files.pythonhosted.org/packages/17/50/bc0394b4ad5b1601be22fa43652173d47e4c9efbf0044c62e9a59b747c56/pytokens-0.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b49750419d300e2b5a3813cf229d4e5a4c728dae470bcc89867a9ad6f25a722d", size = 260824, upload-time = "2026-01-30T01:03:01.471Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/54/3e04f9d92a4be4fc6c80016bc396b923d2a6933ae94b5f557c939c460ee0/pytokens-0.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d9907d61f15bf7261d7e775bd5d7ee4d2930e04424bab1972591918497623a16", size = 264075, upload-time = "2026-01-30T01:03:04.143Z" },
+ { url = "https://files.pythonhosted.org/packages/d1/1b/44b0326cb5470a4375f37988aea5d61b5cc52407143303015ebee94abfd6/pytokens-0.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:ee44d0f85b803321710f9239f335aafe16553b39106384cef8e6de40cb4ef2f6", size = 103323, upload-time = "2026-01-30T01:03:05.412Z" },
+ { url = "https://files.pythonhosted.org/packages/41/5d/e44573011401fb82e9d51e97f1290ceb377800fb4eed650b96f4753b499c/pytokens-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:140709331e846b728475786df8aeb27d24f48cbcf7bcd449f8de75cae7a45083", size = 160663, upload-time = "2026-01-30T01:03:06.473Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/e6/5bbc3019f8e6f21d09c41f8b8654536117e5e211a85d89212d59cbdab381/pytokens-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6d6c4268598f762bc8e91f5dbf2ab2f61f7b95bdc07953b602db879b3c8c18e1", size = 255626, upload-time = "2026-01-30T01:03:08.177Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/3c/2d5297d82286f6f3d92770289fd439956b201c0a4fc7e72efb9b2293758e/pytokens-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:24afde1f53d95348b5a0eb19488661147285ca4dd7ed752bbc3e1c6242a304d1", size = 269779, upload-time = "2026-01-30T01:03:09.756Z" },
+ { url = "https://files.pythonhosted.org/packages/20/01/7436e9ad693cebda0551203e0bf28f7669976c60ad07d6402098208476de/pytokens-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5ad948d085ed6c16413eb5fec6b3e02fa00dc29a2534f088d3302c47eb59adf9", size = 268076, upload-time = "2026-01-30T01:03:10.957Z" },
+ { url = "https://files.pythonhosted.org/packages/2e/df/533c82a3c752ba13ae7ef238b7f8cdd272cf1475f03c63ac6cf3fcfb00b6/pytokens-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:3f901fe783e06e48e8cbdc82d631fca8f118333798193e026a50ce1b3757ea68", size = 103552, upload-time = "2026-01-30T01:03:12.066Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/dc/08b1a080372afda3cceb4f3c0a7ba2bde9d6a5241f1edb02a22a019ee147/pytokens-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8bdb9d0ce90cbf99c525e75a2fa415144fd570a1ba987380190e8b786bc6ef9b", size = 160720, upload-time = "2026-01-30T01:03:13.843Z" },
+ { url = "https://files.pythonhosted.org/packages/64/0c/41ea22205da480837a700e395507e6a24425151dfb7ead73343d6e2d7ffe/pytokens-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5502408cab1cb18e128570f8d598981c68a50d0cbd7c61312a90507cd3a1276f", size = 254204, upload-time = "2026-01-30T01:03:14.886Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/d2/afe5c7f8607018beb99971489dbb846508f1b8f351fcefc225fcf4b2adc0/pytokens-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29d1d8fb1030af4d231789959f21821ab6325e463f0503a61d204343c9b355d1", size = 268423, upload-time = "2026-01-30T01:03:15.936Z" },
+ { url = "https://files.pythonhosted.org/packages/68/d4/00ffdbd370410c04e9591da9220a68dc1693ef7499173eb3e30d06e05ed1/pytokens-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:970b08dd6b86058b6dc07efe9e98414f5102974716232d10f32ff39701e841c4", size = 266859, upload-time = "2026-01-30T01:03:17.458Z" },
+ { url = "https://files.pythonhosted.org/packages/a7/c9/c3161313b4ca0c601eeefabd3d3b576edaa9afdefd32da97210700e47652/pytokens-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:9bd7d7f544d362576be74f9d5901a22f317efc20046efe2034dced238cbbfe78", size = 103520, upload-time = "2026-01-30T01:03:18.652Z" },
+ { url = "https://files.pythonhosted.org/packages/8f/a7/b470f672e6fc5fee0a01d9e75005a0e617e162381974213a945fcd274843/pytokens-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4a14d5f5fc78ce85e426aa159489e2d5961acf0e47575e08f35584009178e321", size = 160821, upload-time = "2026-01-30T01:03:19.684Z" },
+ { url = "https://files.pythonhosted.org/packages/80/98/e83a36fe8d170c911f864bfded690d2542bfcfacb9c649d11a9e6eb9dc41/pytokens-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97f50fd18543be72da51dd505e2ed20d2228c74e0464e4262e4899797803d7fa", size = 254263, upload-time = "2026-01-30T01:03:20.834Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/95/70d7041273890f9f97a24234c00b746e8da86df462620194cef1d411ddeb/pytokens-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc74c035f9bfca0255c1af77ddd2d6ae8419012805453e4b0e7513e17904545d", size = 268071, upload-time = "2026-01-30T01:03:21.888Z" },
+ { url = "https://files.pythonhosted.org/packages/da/79/76e6d09ae19c99404656d7db9c35dfd20f2086f3eb6ecb496b5b31163bad/pytokens-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f66a6bbe741bd431f6d741e617e0f39ec7257ca1f89089593479347cc4d13324", size = 271716, upload-time = "2026-01-30T01:03:23.633Z" },
+ { url = "https://files.pythonhosted.org/packages/79/37/482e55fa1602e0a7ff012661d8c946bafdc05e480ea5a32f4f7e336d4aa9/pytokens-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:b35d7e5ad269804f6697727702da3c517bb8a5228afa450ab0fa787732055fc9", size = 104539, upload-time = "2026-01-30T01:03:24.788Z" },
+ { url = "https://files.pythonhosted.org/packages/30/e8/20e7db907c23f3d63b0be3b8a4fd1927f6da2395f5bcc7f72242bb963dfe/pytokens-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8fcb9ba3709ff77e77f1c7022ff11d13553f3c30299a9fe246a166903e9091eb", size = 168474, upload-time = "2026-01-30T01:03:26.428Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/81/88a95ee9fafdd8f5f3452107748fd04c24930d500b9aba9738f3ade642cc/pytokens-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79fc6b8699564e1f9b521582c35435f1bd32dd06822322ec44afdeba666d8cb3", size = 290473, upload-time = "2026-01-30T01:03:27.415Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/35/3aa899645e29b6375b4aed9f8d21df219e7c958c4c186b465e42ee0a06bf/pytokens-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d31b97b3de0f61571a124a00ffe9a81fb9939146c122c11060725bd5aea79975", size = 303485, upload-time = "2026-01-30T01:03:28.558Z" },
+ { url = "https://files.pythonhosted.org/packages/52/a0/07907b6ff512674d9b201859f7d212298c44933633c946703a20c25e9d81/pytokens-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:967cf6e3fd4adf7de8fc73cd3043754ae79c36475c1c11d514fc72cf5490094a", size = 306698, upload-time = "2026-01-30T01:03:29.653Z" },
+ { url = "https://files.pythonhosted.org/packages/39/2a/cbbf9250020a4a8dd53ba83a46c097b69e5eb49dd14e708f496f548c6612/pytokens-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:584c80c24b078eec1e227079d56dc22ff755e0ba8654d8383b2c549107528918", size = 116287, upload-time = "2026-01-30T01:03:30.912Z" },
+ { url = "https://files.pythonhosted.org/packages/c6/78/397db326746f0a342855b81216ae1f0a32965deccfd7c830a2dbc66d2483/pytokens-0.4.1-py3-none-any.whl", hash = "sha256:26cef14744a8385f35d0e095dc8b3a7583f6c953c2e3d269c7f82484bf5ad2de", size = 13729, upload-time = "2026-01-30T01:03:45.029Z" },
+]
+
+[[package]]
+name = "qrcode"
+version = "8.2"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "colorama", marker = "sys_platform == 'win32'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/8f/b2/7fc2931bfae0af02d5f53b174e9cf701adbb35f39d69c2af63d4a39f81a9/qrcode-8.2.tar.gz", hash = "sha256:35c3f2a4172b33136ab9f6b3ef1c00260dd2f66f858f24d88418a015f446506c", size = 43317, upload-time = "2025-05-01T15:44:24.726Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/dd/b8/d2d6d731733f51684bbf76bf34dab3b70a9148e8f2cef2bb544fccec681a/qrcode-8.2-py3-none-any.whl", hash = "sha256:16e64e0716c14960108e85d853062c9e8bba5ca8252c0b4d0231b9df4060ff4f", size = 45986, upload-time = "2025-05-01T15:44:22.781Z" },
+]
+
+[[package]]
+name = "rsa"
+version = "4.9.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pyasn1" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/da/8a/22b7beea3ee0d44b1916c0c1cb0ee3af23b700b6da9f04991899d0c555d4/rsa-4.9.1.tar.gz", hash = "sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75", size = 29034, upload-time = "2025-04-16T09:51:18.218Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762", size = 34696, upload-time = "2025-04-16T09:51:17.142Z" },
+]
+
+[[package]]
+name = "telethon"
+version = "1.42.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pyaes" },
+ { name = "rsa" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/8c/10/8c8c9476bfce767a856d8aaf9eae8ea1869df4e970da16f1c5b638fd1b0c/telethon-1.42.0.tar.gz", hash = "sha256:032e95511261d5ead719f75494c6c85ece2ce71816b54f3c65d6ccc371d6994d", size = 672734, upload-time = "2025-11-05T19:15:19.849Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e4/e4/8ce0ff55251381966a7c3f88bd5b34abda79b225a8e7fb51ddef3b849c94/telethon-1.42.0-py3-none-any.whl", hash = "sha256:cf361c94586bcacd6d0fc8959a2bce509d1bb37007fe6476a80c4fb4a2decc29", size = 748466, upload-time = "2025-11-05T19:15:18.241Z" },
+]
+
+[[package]]
+name = "tomli"
+version = "2.4.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/82/30/31573e9457673ab10aa432461bee537ce6cef177667deca369efb79df071/tomli-2.4.0.tar.gz", hash = "sha256:aa89c3f6c277dd275d8e243ad24f3b5e701491a860d5121f2cdd399fbb31fc9c", size = 17477, upload-time = "2026-01-11T11:22:38.165Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/3c/d9/3dc2289e1f3b32eb19b9785b6a006b28ee99acb37d1d47f78d4c10e28bf8/tomli-2.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b5ef256a3fd497d4973c11bf142e9ed78b150d36f5773f1ca6088c230ffc5867", size = 153663, upload-time = "2026-01-11T11:21:45.27Z" },
+ { url = "https://files.pythonhosted.org/packages/51/32/ef9f6845e6b9ca392cd3f64f9ec185cc6f09f0a2df3db08cbe8809d1d435/tomli-2.4.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5572e41282d5268eb09a697c89a7bee84fae66511f87533a6f88bd2f7b652da9", size = 148469, upload-time = "2026-01-11T11:21:46.873Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/c2/506e44cce89a8b1b1e047d64bd495c22c9f71f21e05f380f1a950dd9c217/tomli-2.4.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:551e321c6ba03b55676970b47cb1b73f14a0a4dce6a3e1a9458fd6d921d72e95", size = 236039, upload-time = "2026-01-11T11:21:48.503Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/40/e1b65986dbc861b7e986e8ec394598187fa8aee85b1650b01dd925ca0be8/tomli-2.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5e3f639a7a8f10069d0e15408c0b96a2a828cfdec6fca05296ebcdcc28ca7c76", size = 243007, upload-time = "2026-01-11T11:21:49.456Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/6f/6e39ce66b58a5b7ae572a0f4352ff40c71e8573633deda43f6a379d56b3e/tomli-2.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1b168f2731796b045128c45982d3a4874057626da0e2ef1fdd722848b741361d", size = 240875, upload-time = "2026-01-11T11:21:50.755Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/ad/cb089cb190487caa80204d503c7fd0f4d443f90b95cf4ef5cf5aa0f439b0/tomli-2.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:133e93646ec4300d651839d382d63edff11d8978be23da4cc106f5a18b7d0576", size = 246271, upload-time = "2026-01-11T11:21:51.81Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/63/69125220e47fd7a3a27fd0de0c6398c89432fec41bc739823bcc66506af6/tomli-2.4.0-cp311-cp311-win32.whl", hash = "sha256:b6c78bdf37764092d369722d9946cb65b8767bfa4110f902a1b2542d8d173c8a", size = 96770, upload-time = "2026-01-11T11:21:52.647Z" },
+ { url = "https://files.pythonhosted.org/packages/1e/0d/a22bb6c83f83386b0008425a6cd1fa1c14b5f3dd4bad05e98cf3dbbf4a64/tomli-2.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:d3d1654e11d724760cdb37a3d7691f0be9db5fbdaef59c9f532aabf87006dbaa", size = 107626, upload-time = "2026-01-11T11:21:53.459Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/6d/77be674a3485e75cacbf2ddba2b146911477bd887dda9d8c9dfb2f15e871/tomli-2.4.0-cp311-cp311-win_arm64.whl", hash = "sha256:cae9c19ed12d4e8f3ebf46d1a75090e4c0dc16271c5bce1c833ac168f08fb614", size = 94842, upload-time = "2026-01-11T11:21:54.831Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/43/7389a1869f2f26dba52404e1ef13b4784b6b37dac93bac53457e3ff24ca3/tomli-2.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:920b1de295e72887bafa3ad9f7a792f811847d57ea6b1215154030cf131f16b1", size = 154894, upload-time = "2026-01-11T11:21:56.07Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/05/2f9bf110b5294132b2edf13fe6ca6ae456204f3d749f623307cbb7a946f2/tomli-2.4.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7d6d9a4aee98fac3eab4952ad1d73aee87359452d1c086b5ceb43ed02ddb16b8", size = 149053, upload-time = "2026-01-11T11:21:57.467Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/41/1eda3ca1abc6f6154a8db4d714a4d35c4ad90adc0bcf700657291593fbf3/tomli-2.4.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:36b9d05b51e65b254ea6c2585b59d2c4cb91c8a3d91d0ed0f17591a29aaea54a", size = 243481, upload-time = "2026-01-11T11:21:58.661Z" },
+ { url = "https://files.pythonhosted.org/packages/d2/6d/02ff5ab6c8868b41e7d4b987ce2b5f6a51d3335a70aa144edd999e055a01/tomli-2.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c8a885b370751837c029ef9bc014f27d80840e48bac415f3412e6593bbc18c1", size = 251720, upload-time = "2026-01-11T11:22:00.178Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/57/0405c59a909c45d5b6f146107c6d997825aa87568b042042f7a9c0afed34/tomli-2.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8768715ffc41f0008abe25d808c20c3d990f42b6e2e58305d5da280ae7d1fa3b", size = 247014, upload-time = "2026-01-11T11:22:01.238Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/0e/2e37568edd944b4165735687cbaf2fe3648129e440c26d02223672ee0630/tomli-2.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b438885858efd5be02a9a133caf5812b8776ee0c969fea02c45e8e3f296ba51", size = 251820, upload-time = "2026-01-11T11:22:02.727Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/1c/ee3b707fdac82aeeb92d1a113f803cf6d0f37bdca0849cb489553e1f417a/tomli-2.4.0-cp312-cp312-win32.whl", hash = "sha256:0408e3de5ec77cc7f81960c362543cbbd91ef883e3138e81b729fc3eea5b9729", size = 97712, upload-time = "2026-01-11T11:22:03.777Z" },
+ { url = "https://files.pythonhosted.org/packages/69/13/c07a9177d0b3bab7913299b9278845fc6eaaca14a02667c6be0b0a2270c8/tomli-2.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:685306e2cc7da35be4ee914fd34ab801a6acacb061b6a7abca922aaf9ad368da", size = 108296, upload-time = "2026-01-11T11:22:04.86Z" },
+ { url = "https://files.pythonhosted.org/packages/18/27/e267a60bbeeee343bcc279bb9e8fbed0cbe224bc7b2a3dc2975f22809a09/tomli-2.4.0-cp312-cp312-win_arm64.whl", hash = "sha256:5aa48d7c2356055feef06a43611fc401a07337d5b006be13a30f6c58f869e3c3", size = 94553, upload-time = "2026-01-11T11:22:05.854Z" },
+ { url = "https://files.pythonhosted.org/packages/34/91/7f65f9809f2936e1f4ce6268ae1903074563603b2a2bd969ebbda802744f/tomli-2.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84d081fbc252d1b6a982e1870660e7330fb8f90f676f6e78b052ad4e64714bf0", size = 154915, upload-time = "2026-01-11T11:22:06.703Z" },
+ { url = "https://files.pythonhosted.org/packages/20/aa/64dd73a5a849c2e8f216b755599c511badde80e91e9bc2271baa7b2cdbb1/tomli-2.4.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9a08144fa4cba33db5255f9b74f0b89888622109bd2776148f2597447f92a94e", size = 149038, upload-time = "2026-01-11T11:22:07.56Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/8a/6d38870bd3d52c8d1505ce054469a73f73a0fe62c0eaf5dddf61447e32fa/tomli-2.4.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c73add4bb52a206fd0c0723432db123c0c75c280cbd67174dd9d2db228ebb1b4", size = 242245, upload-time = "2026-01-11T11:22:08.344Z" },
+ { url = "https://files.pythonhosted.org/packages/59/bb/8002fadefb64ab2669e5b977df3f5e444febea60e717e755b38bb7c41029/tomli-2.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fb2945cbe303b1419e2706e711b7113da57b7db31ee378d08712d678a34e51e", size = 250335, upload-time = "2026-01-11T11:22:09.951Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/3d/4cdb6f791682b2ea916af2de96121b3cb1284d7c203d97d92d6003e91c8d/tomli-2.4.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:bbb1b10aa643d973366dc2cb1ad94f99c1726a02343d43cbc011edbfac579e7c", size = 245962, upload-time = "2026-01-11T11:22:11.27Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/4a/5f25789f9a460bd858ba9756ff52d0830d825b458e13f754952dd15fb7bb/tomli-2.4.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4cbcb367d44a1f0c2be408758b43e1ffb5308abe0ea222897d6bfc8e8281ef2f", size = 250396, upload-time = "2026-01-11T11:22:12.325Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/2f/b73a36fea58dfa08e8b3a268750e6853a6aac2a349241a905ebd86f3047a/tomli-2.4.0-cp313-cp313-win32.whl", hash = "sha256:7d49c66a7d5e56ac959cb6fc583aff0651094ec071ba9ad43df785abc2320d86", size = 97530, upload-time = "2026-01-11T11:22:13.865Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/af/ca18c134b5d75de7e8dc551c5234eaba2e8e951f6b30139599b53de9c187/tomli-2.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:3cf226acb51d8f1c394c1b310e0e0e61fecdd7adcb78d01e294ac297dd2e7f87", size = 108227, upload-time = "2026-01-11T11:22:15.224Z" },
+ { url = "https://files.pythonhosted.org/packages/22/c3/b386b832f209fee8073c8138ec50f27b4460db2fdae9ffe022df89a57f9b/tomli-2.4.0-cp313-cp313-win_arm64.whl", hash = "sha256:d20b797a5c1ad80c516e41bc1fb0443ddb5006e9aaa7bda2d71978346aeb9132", size = 94748, upload-time = "2026-01-11T11:22:16.009Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/c4/84047a97eb1004418bc10bdbcfebda209fca6338002eba2dc27cc6d13563/tomli-2.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:26ab906a1eb794cd4e103691daa23d95c6919cc2fa9160000ac02370cc9dd3f6", size = 154725, upload-time = "2026-01-11T11:22:17.269Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/5d/d39038e646060b9d76274078cddf146ced86dc2b9e8bbf737ad5983609a0/tomli-2.4.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:20cedb4ee43278bc4f2fee6cb50daec836959aadaf948db5172e776dd3d993fc", size = 148901, upload-time = "2026-01-11T11:22:18.287Z" },
+ { url = "https://files.pythonhosted.org/packages/73/e5/383be1724cb30f4ce44983d249645684a48c435e1cd4f8b5cded8a816d3c/tomli-2.4.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39b0b5d1b6dd03684b3fb276407ebed7090bbec989fa55838c98560c01113b66", size = 243375, upload-time = "2026-01-11T11:22:19.154Z" },
+ { url = "https://files.pythonhosted.org/packages/31/f0/bea80c17971c8d16d3cc109dc3585b0f2ce1036b5f4a8a183789023574f2/tomli-2.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a26d7ff68dfdb9f87a016ecfd1e1c2bacbe3108f4e0f8bcd2228ef9a766c787d", size = 250639, upload-time = "2026-01-11T11:22:20.168Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/8f/2853c36abbb7608e3f945d8a74e32ed3a74ee3a1f468f1ffc7d1cb3abba6/tomli-2.4.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:20ffd184fb1df76a66e34bd1b36b4a4641bd2b82954befa32fe8163e79f1a702", size = 246897, upload-time = "2026-01-11T11:22:21.544Z" },
+ { url = "https://files.pythonhosted.org/packages/49/f0/6c05e3196ed5337b9fe7ea003e95fd3819a840b7a0f2bf5a408ef1dad8ed/tomli-2.4.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:75c2f8bbddf170e8effc98f5e9084a8751f8174ea6ccf4fca5398436e0320bc8", size = 254697, upload-time = "2026-01-11T11:22:23.058Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/f5/2922ef29c9f2951883525def7429967fc4d8208494e5ab524234f06b688b/tomli-2.4.0-cp314-cp314-win32.whl", hash = "sha256:31d556d079d72db7c584c0627ff3a24c5d3fb4f730221d3444f3efb1b2514776", size = 98567, upload-time = "2026-01-11T11:22:24.033Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/31/22b52e2e06dd2a5fdbc3ee73226d763b184ff21fc24e20316a44ccc4d96b/tomli-2.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:43e685b9b2341681907759cf3a04e14d7104b3580f808cfde1dfdb60ada85475", size = 108556, upload-time = "2026-01-11T11:22:25.378Z" },
+ { url = "https://files.pythonhosted.org/packages/48/3d/5058dff3255a3d01b705413f64f4306a141a8fd7a251e5a495e3f192a998/tomli-2.4.0-cp314-cp314-win_arm64.whl", hash = "sha256:3d895d56bd3f82ddd6faaff993c275efc2ff38e52322ea264122d72729dca2b2", size = 96014, upload-time = "2026-01-11T11:22:26.138Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/4e/75dab8586e268424202d3a1997ef6014919c941b50642a1682df43204c22/tomli-2.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:5b5807f3999fb66776dbce568cc9a828544244a8eb84b84b9bafc080c99597b9", size = 163339, upload-time = "2026-01-11T11:22:27.143Z" },
+ { url = "https://files.pythonhosted.org/packages/06/e3/b904d9ab1016829a776d97f163f183a48be6a4deb87304d1e0116a349519/tomli-2.4.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c084ad935abe686bd9c898e62a02a19abfc9760b5a79bc29644463eaf2840cb0", size = 159490, upload-time = "2026-01-11T11:22:28.399Z" },
+ { url = "https://files.pythonhosted.org/packages/e3/5a/fc3622c8b1ad823e8ea98a35e3c632ee316d48f66f80f9708ceb4f2a0322/tomli-2.4.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f2e3955efea4d1cfbcb87bc321e00dc08d2bcb737fd1d5e398af111d86db5df", size = 269398, upload-time = "2026-01-11T11:22:29.345Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/33/62bd6152c8bdd4c305ad9faca48f51d3acb2df1f8791b1477d46ff86e7f8/tomli-2.4.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e0fe8a0b8312acf3a88077a0802565cb09ee34107813bba1c7cd591fa6cfc8d", size = 276515, upload-time = "2026-01-11T11:22:30.327Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/ff/ae53619499f5235ee4211e62a8d7982ba9e439a0fb4f2f351a93d67c1dd2/tomli-2.4.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:413540dce94673591859c4c6f794dfeaa845e98bf35d72ed59636f869ef9f86f", size = 273806, upload-time = "2026-01-11T11:22:32.56Z" },
+ { url = "https://files.pythonhosted.org/packages/47/71/cbca7787fa68d4d0a9f7072821980b39fbb1b6faeb5f5cf02f4a5559fa28/tomli-2.4.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:0dc56fef0e2c1c470aeac5b6ca8cc7b640bb93e92d9803ddaf9ea03e198f5b0b", size = 281340, upload-time = "2026-01-11T11:22:33.505Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/00/d595c120963ad42474cf6ee7771ad0d0e8a49d0f01e29576ee9195d9ecdf/tomli-2.4.0-cp314-cp314t-win32.whl", hash = "sha256:d878f2a6707cc9d53a1be1414bbb419e629c3d6e67f69230217bb663e76b5087", size = 108106, upload-time = "2026-01-11T11:22:34.451Z" },
+ { url = "https://files.pythonhosted.org/packages/de/69/9aa0c6a505c2f80e519b43764f8b4ba93b5a0bbd2d9a9de6e2b24271b9a5/tomli-2.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:2add28aacc7425117ff6364fe9e06a183bb0251b03f986df0e78e974047571fd", size = 120504, upload-time = "2026-01-11T11:22:35.764Z" },
+ { url = "https://files.pythonhosted.org/packages/b3/9f/f1668c281c58cfae01482f7114a4b88d345e4c140386241a1a24dcc9e7bc/tomli-2.4.0-cp314-cp314t-win_arm64.whl", hash = "sha256:2b1e3b80e1d5e52e40e9b924ec43d81570f0e7d09d11081b797bc4692765a3d4", size = 99561, upload-time = "2026-01-11T11:22:36.624Z" },
+ { url = "https://files.pythonhosted.org/packages/23/d1/136eb2cb77520a31e1f64cbae9d33ec6df0d78bdf4160398e86eec8a8754/tomli-2.4.0-py3-none-any.whl", hash = "sha256:1f776e7d669ebceb01dee46484485f43a4048746235e683bcdffacdf1fb4785a", size = 14477, upload-time = "2026-01-11T11:22:37.446Z" },
+]
+
+[[package]]
+name = "typing-extensions"
+version = "4.15.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" },
+]