31 Commits

Author SHA1 Message Date
859c15653c Update README to change repository URLs from GitHub to Gitea
Some checks failed
Safety / security (push) Failing after 17s
Run Tests / test (3.13) (push) Successful in 1m32s
2025-12-21 21:29:07 -06:00
b063c0cbec Update README 2025-12-21 21:26:16 -06:00
3bb64d0533 Update workflow to use older artifact upload and download actions for Gitea support 2025-12-21 21:22:55 -06:00
64fa46f60f Remove Codecov upload step from Gitea workflow for Python 3.13 2025-12-21 21:20:29 -06:00
e05a94e152 move from .github to .gitea 2025-12-21 21:18:51 -06:00
b5ad57ad8f Update LICENSE 2025-12-21 21:16:06 -06:00
3c149cdd0c Update CONTRIBUTING 2025-12-21 21:15:59 -06:00
a1480a5c1b Improve logging and error handling across modules
- Added logging functionality to app.py and rns.py for better error tracking.
- Improved exception handling in RNSManager methods to log specific failures.
- Refactored code in various modules to ensure consistent logging practices.
- Updated UI components to handle exceptions with user feedback.
- Cleaned up formatting in several files for better readability.
2025-11-30 15:55:30 -06:00
1e39fe277e Add 'run' target to Makefile for launching Ren Browser
- Introduced a new 'run' target to the Makefile to start the Ren Browser using Poetry.
- Updated help output to include the new 'run' command for user guidance.
2025-11-30 15:21:28 -06:00
d8de2b1150 Improve RNS management and settings interface in Ren Browser
- Introduced a new rns.py module to encapsulate Reticulum lifecycle management.
- Simplified RNS initialization and error handling in app.py.
- Enhanced settings.py to improve configuration management and user feedback.
- Updated UI components for better interaction and status display.
- Added tests for settings functionality and RNS integration.
2025-11-30 15:21:18 -06:00
d1536aa05a Fix RNS initialization and reload logic in app.py
- Introduced a dedicated storage initialization process for Reticulum.
- More error handling and logging during RNS initialization and reload.
- Updated the reload_reticulum function to support asynchronous execution.
- Modified settings.py to handle reload operations with improved user feedback.
2025-11-30 14:59:02 -06:00
1882325224 Add Android SDK configuration and permissions in pyproject.toml
- Set minimum SDK version to 21 and target SDK version to 34.
- Added necessary permissions for internet access and foreground services.
2025-11-29 10:23:31 -06:00
cce6471534 Update RNS initialization logging in app.py 2025-11-20 17:32:26 -06:00
8bd46f50f3 Update 2025-11-16 07:20:07 -06:00
1e9b53934f Fix windows build 2025-11-16 07:17:50 -06:00
c2921876f7 Update build configuration and dependencies
- Adjusted Python version requirement from 3.13 to 3.11 in pyproject.toml, poetry.lock, and uv.lock.
- Modified Android APK build command in Makefile and GitHub Actions workflow to include package cleanup and exclude the watchdog.
- Updated dependencies in poetry.lock to reflect changes in Python version compatibility.
2025-11-16 01:31:33 -06:00
8d723a8944 Update README 2025-11-16 01:15:08 -06:00
f3d0da08a2 Update GitHub Actions workflow for Android build 2025-11-16 01:04:25 -06:00
5571b810ae Add Windows build support 2025-11-16 00:59:58 -06:00
df72547bde drop docker support 2025-11-16 00:56:22 -06:00
5ec677437e ruff fixes and formatting 2025-11-16 00:46:42 -06:00
3cddaeb2b9 Update:
1. Add basic Micron parser and link support
2. Improve styling/layout
3. Add hot reloading for RNS
2025-11-16 00:34:51 -06:00
e36bfec4a0 Fix Android storage directory detection in StorageManager
- Updated logic to determine storage directory based on ANDROID_DATA and EXTERNAL_STORAGE environment variables.
- Added unit tests to cover new storage directory detection scenarios for Android, including fallback options.
2025-11-15 23:38:39 -06:00
0aaa7938e6 update 2025-11-13 10:04:57 -06:00
379f85c792 add compose 2025-11-12 19:21:02 -06:00
bfbfb22312 move dockerfile 2025-11-12 19:20:58 -06:00
5f9d7784a8 Add docker compose commands 2025-11-12 19:20:47 -06:00
9c0564d253 Update GitHub Actions workflows to use 'master' branch 2025-11-12 19:09:19 -06:00
5da3be18cb Update dependencies in poetry.lock and pyproject.toml
- Added new packages: annotated-doc (0.0.4), annotated-types (0.7.0), arrow (1.4.0), binaryornot (0.4.4), chardet (5.2.0), charset-normalizer (3.4.4), and websockets (15.0.1).
- Updated rns dependency version to (>=1.0.2,<1.5.0) in both pyproject.toml and uv.lock.
- Updated content hash in poetry.lock for consistency.
2025-11-12 19:07:22 -06:00
9eb9aafd35 Update README.md
- Add Poetry instructions back
2025-11-12 18:58:41 -06:00
263e5a92bf Merge pull request 'Tab-Overhaul' (#2) from Tab-Overhaul into master
Reviewed-on: Ivan/Ren-Browser#2
2025-11-03 17:40:41 +00:00
33 changed files with 2234 additions and 588 deletions

View File

@@ -9,6 +9,3 @@ name = "python"
[analyzers.meta]
runtime_version = "3.x.x"
[[analyzers]]
name = "docker"

View File

@@ -1,7 +0,0 @@
config/
__pycache__/
build/
dist/
.ruff_cache/
.env
To-Do.md

View File

@@ -1,14 +1,26 @@
name: Build APK and Linux
name: Build Packages
on:
push:
tags:
- 'v*.*.*'
workflow_dispatch:
inputs:
platform:
description: 'Platform to build'
required: true
type: choice
options:
- all
- linux
- windows
- android
default: 'all'
jobs:
build-linux:
runs-on: ubuntu-latest
if: github.ref_type == 'tag' || github.event.inputs.platform == 'all' || github.event.inputs.platform == 'linux'
permissions:
contents: read
steps:
@@ -33,16 +45,51 @@ jobs:
poetry install --without dev
- name: Build Linux package
run: poetry run flet build linux
run: poetry run flet build linux --no-rich-output
- name: Upload Linux artifact
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
uses: actions/upload-artifact@ff15f0306b3f739f7b6fd43fb5d26cd321bd4de5
with:
name: ren-browser-linux
path: build/linux
build-windows:
runs-on: windows-latest
if: github.ref_type == 'tag' || github.event.inputs.platform == 'all' || github.event.inputs.platform == 'windows'
permissions:
contents: read
steps:
- name: Checkout repository
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8
- name: Set up Python
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
with:
python-version: '3.13'
- name: Install Poetry and dependencies
run: |
python -m pip install --upgrade pip
pip install poetry
poetry config virtualenvs.create false
poetry install --without dev
- name: Build Windows package
run: poetry run flet build windows --no-rich-output
env:
PYTHONIOENCODING: utf-8
PYTHONUTF8: 1
- name: Upload Windows artifact
uses: actions/upload-artifact@ff15f0306b3f739f7b6fd43fb5d26cd321bd4de5
with:
name: ren-browser-windows
path: build/windows
build-android:
runs-on: ubuntu-latest
if: github.ref_type == 'tag' || github.event.inputs.platform == 'all' || github.event.inputs.platform == 'android'
continue-on-error: true
permissions:
contents: read
steps:
@@ -63,7 +110,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
with:
python-version: '3.13'
python-version: '3.12'
- name: Install Poetry and dependencies
run: |
@@ -73,18 +120,18 @@ jobs:
poetry install --without dev
- name: Build Android APK
run: poetry run flet build apk
run: poetry run flet build apk --no-rich-output --exclude watchdog
- name: Upload APK artifact
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
uses: actions/upload-artifact@ff15f0306b3f739f7b6fd43fb5d26cd321bd4de5
with:
name: ren-browser-apk
path: build/apk
create-release:
needs: [build-linux, build-android]
needs: [build-linux, build-windows, build-android]
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/')
if: github.ref_type == 'tag' && !cancelled()
permissions:
contents: write
steps:
@@ -92,13 +139,22 @@ jobs:
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8
- name: Download Linux artifact
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0
if: needs.build-linux.result == 'success'
uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a
with:
name: ren-browser-linux
path: ./artifacts/linux
- name: Download Windows artifact
if: needs.build-windows.result == 'success'
uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a
with:
name: ren-browser-windows
path: ./artifacts/windows
- name: Download APK artifact
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0
if: needs.build-android.result == 'success'
uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a
with:
name: ren-browser-apk
path: ./artifacts/apk
@@ -110,6 +166,7 @@ jobs:
draft: true
files: |
./artifacts/linux/*
./artifacts/windows/*
./artifacts/apk/*
name: Release ${{ github.ref_name }}
body: |
@@ -117,4 +174,5 @@ jobs:
This release contains:
- Linux binary package
- Windows binary package
- Android APK package

View File

@@ -1,7 +1,7 @@
name: Safety
on:
push:
branches: [ main ]
branches: [ master ]
schedule:
- cron: '0 0 * * 0' # weekly
jobs:

View File

@@ -1,12 +1,10 @@
# TODO: Update to use specific commit hashes for the actions for better supply chain security.
name: Run Tests
on:
push:
branches: [ main ]
branches: [ master ]
pull_request:
branches: [ main ]
branches: [ master ]
jobs:
test:
@@ -62,12 +60,3 @@ jobs:
- name: Run tests with pytest
run: |
poetry run pytest -v --cov=ren_browser --cov-report=xml --cov-report=term
- name: Upload coverage reports
uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1
if: matrix.python-version == '3.13'
with:
file: ./coverage.xml
flags: unittests
name: codecov-umbrella
fail_ci_if_error: false

View File

@@ -1,55 +0,0 @@
name: Build and Publish Docker Image
on:
push:
branches: [ main ]
tags: [ 'v*' ]
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435
- name: Log in to the Container registry
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=raw,value=latest,enable={{is_default_branch}}
type=ref,event=branch,prefix=,suffix=,enable={{is_default_branch}}
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=sha,format=short
- name: Build and push Docker image
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
with:
context: .
file: Dockerfile
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max

View File

@@ -9,38 +9,6 @@ I welcome all contributions to the project.
- Micron Renderer/Parser
- Android and Flet (config/permissions/etc)
## Project Structure
Last Updated: 2025-09-28
```
Ren-Browser/
├── ren_browser/ # Main Python application package
│ ├── announces/ # Reticulum network announce handling
│ │ ├── announces.py
│ ├── app.py # Main application entry point
│ ├── controls/ # UI controls and interactions
│ │ ├── shortcuts.py # Keyboard shortcuts handling
│ ├── logs.py # Centralized logging system
│ ├── pages/ # Page fetching and request handling
│ │ ├── page_request.py
│ ├── profiler/ # Performance profiling (placeholder)
│ ├── renderer/ # Content rendering system
│ │ ├── micron.py # Micron markup renderer (WIP)
│ │ └── plaintext.py # Plaintext fallback renderer
│ ├── storage/ # Cross-platform storage management
│ │ ├── storage.py
│ ├── tabs/ # Tab management system
│ │ ├── tabs.py
│ ├── ui/ # User interface components
│ │ ├── settings.py # Settings interface
│ │ └── ui.py # Main UI construction
├── tests/ # Test suite
│ ├── unit/ # Unit tests
│ ├── integration/ # Integration tests
│ └── conftest.py # Test configuration
```
## Rules
1. Be nice to each other.

View File

@@ -1,29 +0,0 @@
ARG PYTHON_VERSION=3.13
FROM python:${PYTHON_VERSION}-alpine
LABEL org.opencontainers.image.source="https://github.com/Sudo-Ivan/Ren-Browser"
LABEL org.opencontainers.image.description="A browser for the Reticulum Network."
LABEL org.opencontainers.image.licenses="MIT"
LABEL org.opencontainers.image.authors="Sudo-Ivan"
WORKDIR /app
RUN apk add --no-cache gcc python3-dev musl-dev linux-headers libffi-dev openssl-dev
RUN pip install --no-cache poetry
ENV POETRY_VIRTUALENVS_IN_PROJECT=true
COPY pyproject.toml poetry.lock* ./
COPY README.md ./
COPY ren_browser ./ren_browser
RUN poetry install --no-interaction --no-ansi --no-cache
ENV PATH="/app/.venv/bin:$PATH"
ENV FLET_WEB_PORT=8550
ENV FLET_WEB_HOST=0.0.0.0
ENV DISPLAY=:99
EXPOSE 8550
ENTRYPOINT ["poetry", "run", "ren-browser-web"]

View File

@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2025 Sudo-Ivan
Copyright (c) 2025 Sudo-Ivan / Quad4.io
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal

View File

@@ -1,5 +1,5 @@
# Ren Browser Makefile
.PHONY: help build poetry-build linux apk docker-build docker-build-multi docker-run docker-stop clean test lint format
.PHONY: help build poetry-build linux apk clean test lint format run
# Default target
help:
@@ -8,12 +8,9 @@ help:
@echo "Available targets:"
@echo " build - Build the project (alias for poetry-build)"
@echo " poetry-build - Build project with Poetry"
@echo " run - Launch Ren Browser via Poetry"
@echo " linux - Build Linux package"
@echo " apk - Build Android APK"
@echo " docker-build - Build Docker image with Buildx"
@echo " docker-build-multi - Build multi-platform Docker image"
@echo " docker-run - Run Docker container"
@echo " docker-stop - Stop Docker container"
@echo " test - Run tests"
@echo " lint - Run linter"
@echo " format - Format code"
@@ -36,25 +33,7 @@ linux:
# Android APK build
apk:
@echo "Building Android APK..."
poetry run flet build apk
# Docker targets
docker-build:
@echo "Building Docker image with Buildx..."
docker buildx build -t ren-browser --load .
docker-build-multi:
@echo "Building multi-platform Docker image..."
docker buildx build -t ren-browser-multi --platform linux/amd64,linux/arm64 --push .
docker-run:
@echo "Running Docker container..."
docker run -p 8550:8550 --name ren-browser-container ren-browser
docker-stop:
@echo "Stopping Docker container..."
docker stop ren-browser-container || true
docker rm ren-browser-container || true
poetry run flet build apk --cleanup-packages --exclude watchdog
# Development targets
test:
@@ -69,6 +48,11 @@ format:
@echo "Formatting code..."
poetry run ruff format .
# Run application
run:
@echo "Starting Ren Browser..."
poetry run ren-browser
# Clean build artifacts
clean:
@echo "Cleaning build artifacts..."
@@ -77,4 +61,3 @@ clean:
rm -rf *.egg-info/
find . -type d -name __pycache__ -exec rm -rf {} +
find . -type f -name "*.pyc" -delete
docker rmi ren-browser || true

View File

@@ -3,9 +3,9 @@
A browser for the [Reticulum Network](https://reticulum.network/).
> [!WARNING]
> This is a work-in-progress.
> This is still a work-in-progress. Please be patient while I work on it.
Target platforms: Web, Linux, Windows, MacOS, Android, iOS.
Due to runner limitations for the time being, I can only build: Linux and Android. Windows and MacOS are coming eventually.
Built using [Flet](https://flet.dev/).
@@ -21,74 +21,109 @@ Built using [Flet](https://flet.dev/).
- Python 3.13+
- Flet
- Reticulum 1.0.0+
- UV
- UV or Poetry
**Setup**
Using UV:
```bash
uv sync
```
Or using Poetry:
```bash
poetry install
```
### Desktop
Using UV:
```bash
# From local development
uv run ren-browser
```
Using Poetry:
```bash
poetry run ren-browser
```
### Web
Using UV:
```bash
# From local development
uv run ren-browser-web
```
Using Poetry:
```bash
poetry run ren-browser-web
```
### Mobile
**Android**
Using UV:
```bash
# From local development
uv run ren-browser-android
```
Using Poetry:
```bash
poetry run ren-browser-android
```
**iOS**
Using UV:
```bash
# From local development
uv run ren-browser-ios
```
Using Poetry:
```bash
poetry run ren-browser-ios
```
To run directly from the GitHub repository without cloning:
```bash
# Using uvx (temporary environment)
uvx --from git+https://github.com/Sudo-Ivan/Ren-Browser.git ren-browser-web
uvx --from git+https://git.quad4.io/Ren/Browser.git ren-browser-web
# Or clone and run locally
git clone https://github.com/Sudo-Ivan/Ren-Browser.git
git clone https://git.quad4.io/Ren/Browser.git
cd Ren-Browser
uv sync
uv run ren-browser-web
```
### Docker/Podman
```bash
docker build -t ren-browser .
docker run -p 8550:8550 -v ./config:/app/config ren-browser
```
## Building
### Linux
Using UV:
```bash
uv run flet build linux
```
Using Poetry:
```bash
poetry run flet build linux
```
### Android
Using UV:
```bash
uv run flet build android
uv run flet build apk
```
Using Poetry:
```bash
poetry run flet build apk
```

View File

@@ -2,7 +2,7 @@
## Bugs
- [ ] Test Config Saving on Android. In my testing and also reported via Email.
- [ ] Test Config Saving on Android.
- [ ] Fix persisting app state in background on Android. https://github.com/Sudo-Ivan/Ren-Browser/issues/1
- [ ] Fix tabs dragging/reordering and overflow issues. https://github.com/Sudo-Ivan/Ren-Browser/issues/1
@@ -41,7 +41,6 @@
## Distribution
- [ ] Add Docker images to build Windows, Linux, MacOS, Android, iOS.
- [ ] Add/Update build workflow to build Windows, MacOS and iOS.
- [ ] Appimage
- [ ] Flatpak

25
poetry.lock generated
View File

@@ -16,6 +16,7 @@ files = [
[package.dependencies]
idna = ">=2.8"
sniffio = ">=1.1"
typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""}
[package.extras]
trio = ["trio (>=0.31.0)"]
@@ -563,6 +564,7 @@ files = [
[package.dependencies]
pytest = ">=8.2,<9"
typing-extensions = {version = ">=4.12", markers = "python_version < \"3.13\""}
[package.extras]
docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1)"]
@@ -623,14 +625,14 @@ six = ">=1.9.0"
[[package]]
name = "rns"
version = "1.0.1"
version = "1.0.2"
description = "Self-configuring, encrypted and resilient mesh networking stack for LoRa, packet radio, WiFi and everything in between"
optional = false
python-versions = ">=3.7"
groups = ["main"]
files = [
{file = "rns-1.0.1-py3-none-any.whl", hash = "sha256:aa77b4c8e1b6899117666e1e55b05b3250416ab5fea2826254358ae320e8b3ed"},
{file = "rns-1.0.1.tar.gz", hash = "sha256:f45ea52b065be09b8e2257425b6fcde1a49899ea41aee349936d182aa1844b26"},
{file = "rns-1.0.2-py3-none-any.whl", hash = "sha256:723bcf0a839025060ff680c4202b09fa766b35093a4a08506bb85485b8a1f154"},
{file = "rns-1.0.2.tar.gz", hash = "sha256:19c025dadc4a85fc37c751e0e892f446456800ca8c434e007c25d8fd6939687e"},
]
[package.dependencies]
@@ -691,7 +693,20 @@ files = [
{file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"},
]
[[package]]
name = "typing-extensions"
version = "4.15.0"
description = "Backported and Experimental Type Hints for Python 3.9+"
optional = false
python-versions = ">=3.9"
groups = ["main", "dev"]
files = [
{file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"},
{file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"},
]
markers = {main = "platform_system != \"Pyodide\" and python_version < \"3.13\"", dev = "python_version < \"3.13\""}
[metadata]
lock-version = "2.1"
python-versions = ">=3.13"
content-hash = "272b66fa2a425d4b1b5dfe2640b2386bf57c447712d60886ed7627ffafd87540"
python-versions = ">=3.11"
content-hash = "8f33d13d6a2aea7ef3e91f7d058cf14c1ab3ec935de8dec09dd979e1f22e48ba"

View File

@@ -7,10 +7,10 @@ authors = [
]
module = "ren_browser.app"
readme = "README.md"
requires-python = ">=3.13"
requires-python = ">=3.11"
dependencies = [
"flet[all] (>=0.28.3,<0.29.0)",
"rns (>=1.0.0,<1.5.0)"
"flet (>=0.28.3,<0.29.0)",
"rns (>=1.0.2,<1.5.0)"
]
[build-system]
@@ -35,6 +35,22 @@ dev = [
"pytest-asyncio>=1.2.0,<2.0.0"
]
[tool.flet]
exclude = ["watchdog"]
[tool.flet.flutter.pubspec.dependency_overrides]
webview_flutter_android = "4.10.1"
[tool.flet.android]
min_sdk_version = 21
target_sdk_version = 34
[tool.flet.android.permission]
"android.permission.INTERNET" = true
"android.permission.ACCESS_NETWORK_STATE" = true
"android.permission.ACCESS_WIFI_STATE" = true
"android.permission.WAKE_LOCK" = true
"android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" = true
"android.permission.FOREGROUND_SERVICE" = true
"android.permission.FOREGROUND_SERVICE_DATA_SYNC" = true

View File

@@ -5,16 +5,22 @@ Ren Browser, a browser for the Reticulum Network built with Flet.
"""
import argparse
import logging
import os
from pathlib import Path
import flet as ft
import RNS
from flet import AppView, Page
from ren_browser import rns
from ren_browser.storage.storage import initialize_storage
from ren_browser.ui.ui import build_ui
RENDERER = "plaintext"
RNS_CONFIG_DIR = None
RNS_INSTANCE = None
logger = logging.getLogger(__name__)
async def main(page: Page):
@@ -49,44 +55,84 @@ async def main(page: Page):
page.add(loader)
page.update()
def init_ret():
import time
initialize_storage(page)
time.sleep(0.5)
config_override = RNS_CONFIG_DIR
# Initialize storage system
storage = initialize_storage(page)
# Get Reticulum config directory from storage manager
config_dir = storage.get_reticulum_config_path()
# Update the global RNS_CONFIG_DIR so RNS uses the right path
global RNS_CONFIG_DIR
RNS_CONFIG_DIR = str(config_dir)
# Ensure any saved config is written to filesystem before RNS init
print("Initializing Reticulum Network...")
try:
saved_config = storage.load_config()
if saved_config and saved_config.strip():
config_file_path = config_dir / "config"
config_file_path.parent.mkdir(parents=True, exist_ok=True)
config_file_path.write_text(saved_config, encoding="utf-8")
except Exception as e:
print(f"Warning: Failed to write config file: {e}")
try:
# Set up logging capture first, before RNS init
import ren_browser.logs
ren_browser.logs.setup_rns_logging()
RNS.Reticulum(str(config_dir))
except (OSError, ValueError):
pass
except Exception:
logger.exception("Unable to configure RNS logging")
success = rns.initialize_reticulum(config_override)
if not success:
error_text = rns.get_last_error() or "Unknown error"
print(f"Error initializing Reticulum: {error_text}")
else:
global RNS_INSTANCE
RNS_INSTANCE = rns.get_reticulum_instance()
config_dir = rns.get_config_path()
if config_dir:
config_path = Path(config_dir)
print(f"RNS config directory: {config_path}")
print(f"Config directory exists: {config_path.exists()}")
print(
"Config directory is writable: "
f"{config_path.is_dir() and os.access(config_path, os.W_OK)}",
)
print("RNS initialized successfully")
page.controls.clear()
build_ui(page)
page.update()
page.run_thread(init_ret)
async def reload_reticulum(page: Page, on_complete=None):
"""Hot reload Reticulum with updated configuration.
Args:
page: Flet page instance
on_complete: Optional callback to run when reload is complete
"""
import asyncio
try:
global RNS_INSTANCE
if RNS_INSTANCE:
try:
RNS_INSTANCE.exit_handler()
print("RNS exit handler completed")
except Exception as e:
print(f"Warning during RNS shutdown: {e}")
rns.shutdown_reticulum()
RNS.Reticulum._Reticulum__instance = None
RNS.Transport.destinations = []
RNS_INSTANCE = None
print("RNS instance cleared")
await asyncio.sleep(0.5)
success = rns.initialize_reticulum(RNS_CONFIG_DIR)
if success:
RNS_INSTANCE = rns.get_reticulum_instance()
if on_complete:
on_complete(True, None)
else:
error_text = rns.get_last_error() or "Unknown error"
print(f"Error reinitializing Reticulum: {error_text}")
if on_complete:
on_complete(False, error_text)
except Exception as e:
print(f"Error during reload: {e}")
if on_complete:
on_complete(False, str(e))
def run():
@@ -101,10 +147,17 @@ def run():
help="Select renderer (plaintext or micron)",
)
parser.add_argument(
"-w", "--web", action="store_true", help="Launch in web browser mode"
"-w",
"--web",
action="store_true",
help="Launch in web browser mode",
)
parser.add_argument(
"-p", "--port", type=int, default=None, help="Port for web server"
"-p",
"--port",
type=int,
default=None,
help="Port for web server",
)
parser.add_argument(
"-c",
@@ -120,9 +173,7 @@ def run():
if args.config_dir:
RNS_CONFIG_DIR = args.config_dir
else:
import pathlib
RNS_CONFIG_DIR = str(pathlib.Path.home() / ".reticulum")
RNS_CONFIG_DIR = None
if args.web:
if args.port is not None:

View File

@@ -45,7 +45,7 @@ class PageFetcher:
"""
RNS.log(
f"PageFetcher: starting fetch of {req.page_path} from {req.destination_hash}"
f"PageFetcher: starting fetch of {req.page_path} from {req.destination_hash}",
)
dest_bytes = bytes.fromhex(req.destination_hash)
if not RNS.Transport.has_path(dest_bytes):
@@ -87,11 +87,11 @@ class PageFetcher:
req.field_data,
response_callback=on_response,
failed_callback=on_failed,
)
),
)
ev.wait(timeout=15)
data_str = result["data"] or "No content received"
RNS.log(
f"PageFetcher: received data for {req.destination_hash}:{req.page_path}"
f"PageFetcher: received data for {req.destination_hash}:{req.page_path}",
)
return data_str

View File

@@ -1,27 +1,295 @@
"""Micron markup renderer for Ren Browser.
Provides rendering capabilities for micron markup content,
currently implemented as a placeholder.
Provides rendering capabilities for micron markup content.
"""
import re
import flet as ft
from ren_browser.renderer.plaintext import render_plaintext
def render_micron(content: str) -> ft.Control:
"""Render micron markup content to a Flet control placeholder.
Currently displays raw content.
def hex_to_rgb(hex_color: str) -> str:
"""Convert 3-char hex color to RGB string."""
if len(hex_color) != 3:
return "255,255,255"
r = int(hex_color[0], 16) * 17
g = int(hex_color[1], 16) * 17
b = int(hex_color[2], 16) * 17
return f"{r},{g},{b}"
def parse_micron_line(line: str) -> list:
"""Parse a single line of micron markup into styled text spans.
Returns list of dicts with 'text', 'bold', 'italic', 'underline', 'color', 'bgcolor'.
"""
spans = []
current_text = ""
bold = False
italic = False
underline = False
color = None
bgcolor = None
i = 0
while i < len(line):
if line[i] == "`" and i + 1 < len(line):
if current_text:
spans.append(
{
"text": current_text,
"bold": bold,
"italic": italic,
"underline": underline,
"color": color,
"bgcolor": bgcolor,
},
)
current_text = ""
tag = line[i + 1]
if tag == "!":
bold = not bold
i += 2
elif tag == "*":
italic = not italic
i += 2
elif tag == "_":
underline = not underline
i += 2
elif tag == "F" and i + 5 <= len(line):
color = hex_to_rgb(line[i + 2 : i + 5])
i += 5
elif tag == "f":
color = None
i += 2
elif tag == "B" and i + 5 <= len(line):
bgcolor = hex_to_rgb(line[i + 2 : i + 5])
i += 5
elif tag == "b":
bgcolor = None
i += 2
elif tag == "`":
bold = False
italic = False
underline = False
color = None
bgcolor = None
i += 2
else:
current_text += line[i]
i += 1
else:
current_text += line[i]
i += 1
if current_text:
spans.append(
{
"text": current_text,
"bold": bold,
"italic": italic,
"underline": underline,
"color": color,
"bgcolor": bgcolor,
},
)
return spans
def render_micron(content: str, on_link_click=None) -> ft.Control:
"""Render micron markup content to a Flet control.
Falls back to plaintext renderer if parsing fails.
Args:
content: Micron markup content to render.
on_link_click: Optional callback function(url) called when a link is clicked.
Returns:
ft.Control: Rendered content as a Flet control.
"""
return ft.Text(
content,
selectable=True,
font_family="monospace",
try:
return _render_micron_internal(content, on_link_click)
except Exception as e:
print(f"Micron rendering failed: {e}, falling back to plaintext")
return render_plaintext(content)
def _render_micron_internal(content: str, on_link_click=None) -> ft.Control:
"""Internal micron rendering implementation.
Args:
content: Micron markup content to render.
on_link_click: Optional callback function(url) called when a link is clicked.
Returns:
ft.Control: Rendered content as a Flet control.
"""
lines = content.split("\n")
controls = []
section_level = 0
alignment = ft.TextAlign.LEFT
for line in lines:
if not line:
controls.append(ft.Container(height=10))
continue
if line.startswith("#"):
continue
if line.startswith("`c"):
alignment = ft.TextAlign.CENTER
line = line[2:]
elif line.startswith("`l"):
alignment = ft.TextAlign.LEFT
line = line[2:]
elif line.startswith("`r"):
alignment = ft.TextAlign.RIGHT
line = line[2:]
elif line.startswith("`a"):
alignment = ft.TextAlign.LEFT
line = line[2:]
if line.startswith(">"):
level = 0
while level < len(line) and line[level] == ">":
level += 1
section_level = level
heading_text = line[level:].strip()
if heading_text:
controls.append(
ft.Container(
content=ft.Text(
heading_text,
size=20 - (level * 2),
weight=ft.FontWeight.BOLD,
color=ft.Colors.BLUE_400,
),
padding=ft.padding.only(left=level * 20, top=10, bottom=5),
),
)
continue
if line.strip() == "-":
controls.append(
ft.Container(
content=ft.Divider(color=ft.Colors.GREY_700),
padding=ft.padding.only(left=section_level * 20),
),
)
continue
if "`[" in line:
row_controls = []
last_end = 0
for link_match in re.finditer(r"`\[([^`]*)`([^\]]*)\]", line):
before = line[last_end : link_match.start()]
if before:
before_spans = parse_micron_line(before)
row_controls.extend(
create_text_span(span) for span in before_spans
)
label = link_match.group(1)
url = link_match.group(2)
def make_link_handler(link_url):
def handler(e):
if on_link_click:
on_link_click(link_url)
return handler
row_controls.append(
ft.TextButton(
text=label if label else url,
style=ft.ButtonStyle(
color=ft.Colors.BLUE_400,
overlay_color=ft.Colors.BLUE_900,
),
on_click=make_link_handler(url),
),
)
last_end = link_match.end()
after = line[last_end:]
if after:
after_spans = parse_micron_line(after)
row_controls.extend(
create_text_span(span) for span in after_spans
)
if row_controls:
controls.append(
ft.Container(
content=ft.Row(
controls=row_controls,
spacing=0,
wrap=True,
),
padding=ft.padding.only(left=section_level * 20),
),
)
continue
spans = parse_micron_line(line)
if spans:
text_controls = [create_text_span(span) for span in spans]
controls.append(
ft.Container(
content=ft.Row(
controls=text_controls,
spacing=0,
wrap=True,
alignment=alignment,
),
padding=ft.padding.only(left=section_level * 20),
),
)
return ft.Column(
controls=controls,
spacing=5,
scroll=ft.ScrollMode.AUTO,
expand=True,
)
def create_text_span(span: dict) -> ft.Text:
"""Create a Text control from a span dict."""
styles = []
if span["bold"]:
styles.append(ft.TextStyle(weight=ft.FontWeight.BOLD))
if span["italic"]:
styles.append(ft.TextStyle(italic=True))
text_decoration = ft.TextDecoration.UNDERLINE if span["underline"] else None
color = span["color"]
bgcolor = span["bgcolor"]
text_style = ft.TextStyle(
weight=ft.FontWeight.BOLD if span["bold"] else None,
italic=span["italic"] if span["italic"] else None,
decoration=text_decoration,
)
return ft.Text(
span["text"],
style=text_style,
color=f"rgb({color})" if color else None,
bgcolor=f"rgb({bgcolor})" if bgcolor else None,
selectable=True,
no_wrap=False,
)

289
ren_browser/rns.py Normal file
View File

@@ -0,0 +1,289 @@
"""Reticulum helper utilities for Ren Browser."""
from __future__ import annotations
import logging
import os
import tempfile
from pathlib import Path
import RNS
logger = logging.getLogger(__name__)
class RNSManager:
"""Manage Reticulum lifecycle and configuration."""
def __init__(self):
self.reticulum = None
self.config_path: str | None = None
self.last_error: str | None = None
def _is_android(self) -> bool:
vendor = getattr(RNS, "vendor", None)
platformutils = getattr(vendor, "platformutils", None)
if platformutils and hasattr(platformutils, "is_android"):
try:
return bool(platformutils.is_android())
except Exception:
return False
return "ANDROID_ROOT" in os.environ
def _android_storage_root(self) -> Path:
candidates = [
os.environ.get("ANDROID_APP_PATH"),
os.environ.get("ANDROID_PRIVATE"),
os.environ.get("ANDROID_ARGUMENT"),
]
for raw_path in candidates:
if not raw_path:
continue
path = Path(raw_path).expanduser()
if path.name == "app":
path = path.parent
if path.is_file():
path = path.parent
if path.is_dir():
return path
return Path(tempfile.gettempdir())
def _default_config_root(self) -> Path:
override = os.environ.get("REN_BROWSER_RNS_DIR") or os.environ.get(
"REN_RETICULUM_CONFIG_DIR",
)
if override:
return Path(override).expanduser()
if self._is_android():
return self._android_storage_root() / "ren_browser" / "reticulum"
return Path.home() / ".reticulum"
def _resolve_config_dir(self, preferred: str | Path | None) -> Path:
target = (
Path(preferred).expanduser() if preferred else self._default_config_root()
)
allow_fallback = preferred is None
try:
target.mkdir(parents=True, exist_ok=True)
except Exception:
if not allow_fallback:
raise
fallback = Path(tempfile.gettempdir()) / "ren_browser" / "reticulum"
fallback.mkdir(parents=True, exist_ok=True)
target = fallback
self._seed_config_if_missing(target)
return target
def _default_tcp_interfaces_snippet(self) -> str:
return """
[[Quad4 Node 1]]
type = TCPClientInterface
interface_enabled = true
target_host = rns.quad4.io
target_port = 4242
name = Quad4 Node 1
selected_interface_mode = 1
[[Quad4 Node 2]]
type = TCPClientInterface
interface_enabled = true
target_host = rns2.quad4.io
target_port = 4242
name = Quad4 Node 2
selected_interface_mode = 1
""".strip(
"\n",
)
def _seed_config_if_missing(self, target: Path) -> None:
config_file = target / "config"
if config_file.exists():
return
base_content = None
try:
default_lines = getattr(RNS.Reticulum, "__default_rns_config__", None)
if default_lines:
if isinstance(default_lines, list):
base_content = "\n".join(default_lines)
else:
base_content = str(default_lines)
except Exception:
base_content = None
if not base_content:
base_content = (
"[reticulum]\n"
"share_instance = Yes\n\n"
"[interfaces]\n\n"
" [[Default Interface]]\n"
" type = AutoInterface\n"
" enabled = Yes\n"
)
snippet = self._default_tcp_interfaces_snippet()
if snippet and snippet not in base_content:
base_content = base_content.rstrip() + "\n\n" + snippet + "\n"
try:
config_file.write_text(base_content, encoding="utf-8")
os.chmod(config_file, 0o600)
except Exception:
logger.exception("Failed to seed default config at %s", config_file)
def _ensure_default_tcp_interfaces(self) -> None:
if not self.config_path:
return
config_file = Path(self.config_path) / "config"
if not config_file.exists():
return
try:
content = config_file.read_text(encoding="utf-8")
except Exception:
return
snippet = self._default_tcp_interfaces_snippet()
if "target_host = rns.quad4.io" in content or "Quad4 Node 1" in content:
return
try:
with open(config_file, "a", encoding="utf-8") as cfg:
if not content.endswith("\n"):
cfg.write("\n")
cfg.write("\n" + snippet + "\n")
except Exception:
logger.exception(
"Failed to append default TCP interfaces to %s",
config_file,
)
def _get_or_create_config_dir(self) -> Path:
if self.config_path:
return Path(self.config_path)
resolved = self._resolve_config_dir(None)
self.config_path = str(resolved)
return resolved
def initialize(self, config_dir: str | None = None) -> bool:
"""Initialize the Reticulum instance."""
self.last_error = None
try:
use_custom_dir = bool(config_dir or self._is_android())
if use_custom_dir:
resolved = self._resolve_config_dir(config_dir)
self.config_path = str(resolved)
self.reticulum = RNS.Reticulum(configdir=self.config_path)
else:
self.reticulum = RNS.Reticulum()
self.config_path = getattr(
RNS.Reticulum,
"configdir",
str(Path.home() / ".reticulum"),
)
self._ensure_default_tcp_interfaces()
return True
except Exception as exc:
self.last_error = str(exc)
return False
def shutdown(self) -> bool:
"""Shut down the active Reticulum instance."""
try:
if self.reticulum and hasattr(self.reticulum, "exit_handler"):
self.reticulum.exit_handler()
except Exception:
return False
finally:
self.reticulum = None
return True
def read_config_file(self) -> str:
"""Return the current configuration file contents."""
config_dir = self._get_or_create_config_dir()
config_file = config_dir / "config"
try:
return config_file.read_text(encoding="utf-8")
except FileNotFoundError:
self._seed_config_if_missing(config_dir)
try:
return config_file.read_text(encoding="utf-8")
except Exception:
return ""
except Exception:
return ""
def write_config_file(self, content: str) -> bool:
"""Persist configuration text to disk."""
config_dir = self._get_or_create_config_dir()
config_file = config_dir / "config"
try:
config_dir.mkdir(parents=True, exist_ok=True)
config_file.write_text(content, encoding="utf-8")
os.chmod(config_file, 0o600)
return True
except Exception as exc:
self.last_error = str(exc)
return False
def get_config_path(self) -> str | None:
"""Return the directory holding the active Reticulum config."""
if self.config_path:
return self.config_path
try:
default_path = self._resolve_config_dir(None)
self.config_path = str(default_path)
return self.config_path
except Exception:
return None
def get_reticulum_instance(self):
"""Return the current Reticulum instance, if any."""
return self.reticulum
def get_last_error(self) -> str | None:
"""Return the last recorded error string."""
return self.last_error
rns_manager = RNSManager()
def initialize_reticulum(config_dir: str | None = None) -> bool:
"""Initialize Reticulum using the shared manager."""
return rns_manager.initialize(config_dir)
def shutdown_reticulum() -> bool:
"""Shut down the shared Reticulum instance."""
return rns_manager.shutdown()
def get_reticulum_instance():
"""Expose the active Reticulum instance."""
return rns_manager.get_reticulum_instance()
def get_config_path() -> str | None:
"""Expose the active configuration directory."""
return rns_manager.get_config_path()
def read_config_file() -> str:
"""Read the Reticulum configuration file."""
return rns_manager.read_config_file()
def write_config_file(content: str) -> bool:
"""Write the Reticulum configuration file."""
return rns_manager.write_config_file(content)
def get_last_error() -> str | None:
"""Return the last recorded Reticulum error."""
return rns_manager.get_last_error()

View File

@@ -7,7 +7,7 @@ and other application data across different platforms.
import json
import os
import pathlib
from typing import Any, Dict, Optional
from typing import Any
import flet as ft
@@ -19,7 +19,7 @@ class StorageManager:
with platform-specific storage locations.
"""
def __init__(self, page: Optional[ft.Page] = None):
def __init__(self, page: ft.Page | None = None):
"""Initialize storage manager.
Args:
@@ -37,21 +37,21 @@ class StorageManager:
pass
if os.name == "posix" and "ANDROID_ROOT" in os.environ:
# Android - use user-accessible external storage
storage_dir = pathlib.Path("/storage/emulated/0/Documents/ren_browser")
elif hasattr(os, "uname") and "iOS" in str(
getattr(os, "uname", lambda: "")()
).replace("iPhone", "iOS"):
# iOS - use app's documents directory
storage_dir = pathlib.Path.home() / "Documents" / "ren_browser"
if "ANDROID_DATA" in os.environ:
storage_dir = pathlib.Path(os.environ["ANDROID_DATA"]) / "ren_browser"
elif "EXTERNAL_STORAGE" in os.environ:
ext_storage = pathlib.Path(os.environ["EXTERNAL_STORAGE"])
storage_dir = ext_storage / "ren_browser"
else:
# Desktop (Linux, Windows, macOS) - use home directory
if "APPDATA" in os.environ: # Windows
storage_dir = pathlib.Path("/data/local/tmp/ren_browser")
elif hasattr(os, "uname") and "iOS" in str(
getattr(os, "uname", lambda: "")(),
).replace("iPhone", "iOS"):
storage_dir = pathlib.Path.home() / "Documents" / "ren_browser"
elif "APPDATA" in os.environ: # Windows
storage_dir = pathlib.Path(os.environ["APPDATA"]) / "ren_browser"
elif "XDG_CONFIG_HOME" in os.environ: # Linux XDG standard
storage_dir = (
pathlib.Path(os.environ["XDG_CONFIG_HOME"]) / "ren_browser"
)
storage_dir = pathlib.Path(os.environ["XDG_CONFIG_HOME"]) / "ren_browser"
else:
storage_dir = pathlib.Path.home() / ".ren_browser"
@@ -124,7 +124,8 @@ class StorageManager:
if self.page and hasattr(self.page, "client_storage"):
self.page.client_storage.set("ren_browser_config", config_content)
self.page.client_storage.set(
"ren_browser_config_error", f"File save failed: {error}"
"ren_browser_config_error",
f"File save failed: {error}",
)
return True
@@ -191,7 +192,8 @@ class StorageManager:
if self.page and hasattr(self.page, "client_storage"):
self.page.client_storage.set(
"ren_browser_bookmarks", json.dumps(bookmarks)
"ren_browser_bookmarks",
json.dumps(bookmarks),
)
return True
@@ -203,7 +205,7 @@ class StorageManager:
try:
bookmarks_path = self._storage_dir / "bookmarks.json"
if bookmarks_path.exists():
with open(bookmarks_path, "r", encoding="utf-8") as f:
with open(bookmarks_path, encoding="utf-8") as f:
return json.load(f)
if self.page and hasattr(self.page, "client_storage"):
@@ -235,7 +237,7 @@ class StorageManager:
try:
history_path = self._storage_dir / "history.json"
if history_path.exists():
with open(history_path, "r", encoding="utf-8") as f:
with open(history_path, encoding="utf-8") as f:
return json.load(f)
if self.page and hasattr(self.page, "client_storage"):
@@ -248,7 +250,49 @@ class StorageManager:
return []
def get_storage_info(self) -> Dict[str, Any]:
def save_app_settings(self, settings: dict) -> bool:
"""Save application settings to storage."""
try:
settings_path = self._storage_dir / "settings.json"
with open(settings_path, "w", encoding="utf-8") as f:
json.dump(settings, f, indent=2)
if self.page and hasattr(self.page, "client_storage"):
self.page.client_storage.set(
"ren_browser_settings",
json.dumps(settings),
)
return True
except Exception:
return False
def load_app_settings(self) -> dict:
"""Load application settings from storage."""
default_settings = {
"horizontal_scroll": False,
"page_bgcolor": "#000000",
}
try:
settings_path = self._storage_dir / "settings.json"
if settings_path.exists():
with open(settings_path, encoding="utf-8") as f:
loaded = json.load(f)
return {**default_settings, **loaded}
if self.page and hasattr(self.page, "client_storage"):
stored_settings = self.page.client_storage.get("ren_browser_settings")
if stored_settings and isinstance(stored_settings, str):
loaded = json.loads(stored_settings)
return {**default_settings, **loaded}
except (OSError, json.JSONDecodeError, TypeError):
pass
return default_settings
def get_storage_info(self) -> dict[str, Any]:
"""Get information about the storage system."""
return {
"storage_dir": str(self._storage_dir),
@@ -272,10 +316,10 @@ class StorageManager:
# Global storage instance
_storage_manager: Optional[StorageManager] = None
_storage_manager: StorageManager | None = None
def get_storage_manager(page: Optional[ft.Page] = None) -> StorageManager:
def get_storage_manager(page: ft.Page | None = None) -> StorageManager:
"""Get the global storage manager instance."""
global _storage_manager
if _storage_manager is None:

View File

@@ -8,8 +8,10 @@ from types import SimpleNamespace
import flet as ft
from ren_browser.pages.page_request import PageFetcher, PageRequest
from ren_browser.renderer.micron import render_micron
from ren_browser.renderer.plaintext import render_plaintext
from ren_browser.storage.storage import get_storage_manager
class TabsManager:
@@ -30,28 +32,56 @@ class TabsManager:
self.page = page
self.page.on_resize = self._on_resize
self.manager = SimpleNamespace(tabs=[], index=0)
self.tab_bar = ft.Row(
spacing=4,
storage = get_storage_manager(page)
self.settings = storage.load_app_settings()
self.tab_bar = ft.Container(
content=ft.Row(
spacing=6,
scroll=ft.ScrollMode.AUTO,
),
padding=ft.padding.symmetric(horizontal=8, vertical=8),
)
self.overflow_menu = None
self.content_container = ft.Container(
expand=True, bgcolor=ft.Colors.BLACK, padding=ft.padding.all(5),
expand=True,
bgcolor=self.settings.get("page_bgcolor", ft.Colors.BLACK),
padding=ft.padding.all(16),
)
def handle_link_click_home(link_url):
if len(self.manager.tabs) > 0:
tab = self.manager.tabs[0]
full_url = link_url
if ":" not in link_url:
full_url = f"{link_url}:/page/index.mu"
tab["url_field"].value = full_url
self._on_tab_go(None, 0)
default_content = (
render_micron("Welcome to Ren Browser")
render_micron(
"Welcome to Ren Browser",
on_link_click=handle_link_click_home,
)
if app_module.RENDERER == "micron"
else render_plaintext("Welcome to Ren Browser")
)
self._add_tab_internal("Home", default_content)
self.add_btn = ft.IconButton(
ft.Icons.ADD, tooltip="New Tab", on_click=self._on_add_click,
ft.Icons.ADD,
tooltip="New Tab",
on_click=self._on_add_click,
icon_color=ft.Colors.WHITE,
)
self.close_btn = ft.IconButton(
ft.Icons.CLOSE, tooltip="Close Tab", on_click=self._on_close_click,
ft.Icons.CLOSE,
tooltip="Close Tab",
on_click=self._on_close_click,
icon_color=ft.Colors.WHITE,
)
self.tab_bar.controls.append(self.add_btn)
self.tab_bar.controls.append(self.close_btn)
self.tab_bar.content.controls.append(self.add_btn)
self.tab_bar.content.controls.append(self.close_btn)
self.select_tab(0)
self._update_tab_visibility()
@@ -59,6 +89,30 @@ class TabsManager:
"""Handle page resize event and update tab visibility."""
self._update_tab_visibility()
def apply_settings(self, settings: dict) -> None:
"""Apply appearance settings to the tab manager.
Args:
settings: Dictionary containing appearance settings.
"""
self.settings = settings
bgcolor = settings.get("page_bgcolor", "#000000")
self.content_container.bgcolor = bgcolor
horizontal_scroll = settings.get("horizontal_scroll", False)
scroll_mode = ft.ScrollMode.ALWAYS if horizontal_scroll else ft.ScrollMode.AUTO
for tab in self.manager.tabs:
if "content" in tab and hasattr(tab["content"], "scroll"):
tab["content"].scroll = scroll_mode
if "content_control" in tab and hasattr(tab["content_control"], "scroll"):
tab["content_control"].scroll = scroll_mode
if self.content_container.content:
self.content_container.content.update()
self.page.update()
def _update_tab_visibility(self) -> None:
"""Dynamically adjust tab visibility based on page width.
@@ -67,23 +121,22 @@ class TabsManager:
if not self.page.width or self.page.width == 0:
return
if self.overflow_menu and self.overflow_menu in self.tab_bar.controls:
self.tab_bar.controls.remove(self.overflow_menu)
if self.overflow_menu and self.overflow_menu in self.tab_bar.content.controls:
self.tab_bar.content.controls.remove(self.overflow_menu)
self.overflow_menu = None
"""Estimate available width for tabs (Page width - buttons - padding)."""
available_width = self.page.width - 100
cumulative_width = 0
visible_tabs_count = 0
tab_containers = [c for c in self.tab_bar.controls if isinstance(c, ft.Container)]
tab_containers = [
c for c in self.tab_bar.content.controls if isinstance(c, ft.Container)
]
for i, tab in enumerate(self.manager.tabs):
"""Estimate tab width: (char count * avg char width) + padding + spacing."""
estimated_width = len(tab["title"]) * 10 + 32 + self.tab_bar.spacing
estimated_width = len(tab["title"]) * 10 + 32 + self.tab_bar.content.spacing
"""Always show at least one tab."""
if cumulative_width + estimated_width <= available_width or i == 0:
cumulative_width += estimated_width
if i < len(tab_containers):
@@ -93,7 +146,6 @@ class TabsManager:
tab_containers[i].visible = False
if len(self.manager.tabs) > visible_tabs_count:
"""Move extra tabs to overflow menu."""
overflow_items = []
for i in range(visible_tabs_count, len(self.manager.tabs)):
tab_data = self.manager.tabs[i]
@@ -110,7 +162,7 @@ class TabsManager:
items=overflow_items,
)
self.tab_bar.controls.insert(visible_tabs_count, self.overflow_menu)
self.tab_bar.content.controls.insert(visible_tabs_count, self.overflow_menu)
def _add_tab_internal(self, title: str, content: ft.Control) -> None:
"""Add a new tab to the manager with the given title and content."""
@@ -118,17 +170,28 @@ class TabsManager:
url_field = ft.TextField(
value=title,
expand=True,
text_style=ft.TextStyle(size=12),
content_padding=ft.padding.only(top=8, bottom=8, left=8, right=8),
text_style=ft.TextStyle(size=14),
content_padding=ft.padding.symmetric(horizontal=16, vertical=12),
border_radius=24,
border_color=ft.Colors.GREY_700,
focused_border_color=ft.Colors.BLUE_400,
bgcolor=ft.Colors.GREY_800,
prefix_icon=ft.Icons.SEARCH,
)
go_btn = ft.IconButton(
ft.Icons.OPEN_IN_BROWSER,
tooltip="Load URL",
ft.Icons.ARROW_FORWARD,
tooltip="Go",
on_click=lambda e, i=idx: self._on_tab_go(e, i),
icon_color=ft.Colors.BLUE_400,
bgcolor=ft.Colors.BLUE_900,
)
content_control = content
horizontal_scroll = self.settings.get("horizontal_scroll", False)
scroll_mode = ft.ScrollMode.ALWAYS if horizontal_scroll else ft.ScrollMode.AUTO
tab_content = ft.Column(
expand=True,
scroll=scroll_mode,
controls=[
content_control,
],
@@ -143,15 +206,26 @@ class TabsManager:
},
)
tab_container = ft.Container(
content=ft.Text(title),
content=ft.Row(
controls=[
ft.Text(
title,
size=13,
weight=ft.FontWeight.W_500,
overflow=ft.TextOverflow.ELLIPSIS,
),
],
spacing=8,
),
on_click=lambda e, i=idx: self.select_tab(i), # type: ignore
padding=ft.padding.symmetric(horizontal=12, vertical=6),
border_radius=5,
bgcolor=ft.Colors.SURFACE_CONTAINER_HIGHEST,
padding=ft.padding.symmetric(horizontal=16, vertical=10),
border_radius=8,
bgcolor=ft.Colors.GREY_800,
ink=True,
width=150,
)
"""Insert the new tab before the add/close buttons."""
insert_pos = max(0, len(self.tab_bar.controls) - 2)
self.tab_bar.controls.insert(insert_pos, tab_container)
insert_pos = max(0, len(self.tab_bar.content.controls) - 2)
self.tab_bar.content.controls.insert(insert_pos, tab_container)
self._update_tab_visibility()
def _on_add_click(self, e) -> None: # type: ignore
@@ -160,8 +234,18 @@ class TabsManager:
content_text = f"Content for {title}"
import ren_browser.app as app_module
new_idx = len(self.manager.tabs)
def handle_link_click_new(link_url):
tab = self.manager.tabs[new_idx]
full_url = link_url
if ":" not in link_url:
full_url = f"{link_url}:/page/index.mu"
tab["url_field"].value = full_url
self._on_tab_go(None, new_idx)
content = (
render_micron(content_text)
render_micron(content_text, on_link_click=handle_link_click_new)
if app_module.RENDERER == "micron"
else render_plaintext(content_text)
)
@@ -175,13 +259,17 @@ class TabsManager:
return
idx = self.manager.index
tab_containers = [c for c in self.tab_bar.controls if isinstance(c, ft.Container)]
tab_containers = [
c for c in self.tab_bar.content.controls if isinstance(c, ft.Container)
]
control_to_remove = tab_containers[idx]
self.manager.tabs.pop(idx)
self.tab_bar.controls.remove(control_to_remove)
self.tab_bar.content.controls.remove(control_to_remove)
updated_tab_containers = [c for c in self.tab_bar.controls if isinstance(c, ft.Container)]
updated_tab_containers = [
c for c in self.tab_bar.content.controls if isinstance(c, ft.Container)
]
for i, control in enumerate(updated_tab_containers):
control.on_click = lambda e, i=i: self.select_tab(i) # type: ignore
@@ -199,12 +287,16 @@ class TabsManager:
"""
self.manager.index = idx
tab_containers = [c for c in self.tab_bar.controls if isinstance(c, ft.Container)]
tab_containers = [
c for c in self.tab_bar.content.controls if isinstance(c, ft.Container)
]
for i, control in enumerate(tab_containers):
if i == idx:
control.bgcolor = ft.Colors.PRIMARY_CONTAINER
control.bgcolor = ft.Colors.BLUE_900
control.border = ft.border.all(2, ft.Colors.BLUE_400)
else:
control.bgcolor = ft.Colors.SURFACE_CONTAINER_HIGHEST
control.bgcolor = ft.Colors.GREY_800
control.border = None
self.content_container.content = self.manager.tabs[idx]["content"]
self.page.update()
@@ -215,16 +307,68 @@ class TabsManager:
url = tab["url_field"].value.strip()
if not url:
return
placeholder_text = f"Loading content for {url}"
placeholder_text = f"Loading content for {url}..."
import ren_browser.app as app_module
new_control = (
render_micron(placeholder_text)
current_node_hash = None
if ":" in url:
current_node_hash = url.split(":")[0]
def handle_link_click(link_url):
full_url = link_url
if ":" not in link_url:
full_url = f"{link_url}:/page/index.mu"
elif link_url.startswith(":/"):
if current_node_hash:
full_url = f"{current_node_hash}{link_url}"
else:
full_url = link_url
tab["url_field"].value = full_url
self._on_tab_go(None, idx)
placeholder_control = (
render_micron(placeholder_text, on_link_click=handle_link_click)
if app_module.RENDERER == "micron"
else render_plaintext(placeholder_text)
)
tab["content_control"] = placeholder_control
tab["content"].controls[0] = placeholder_control
if self.manager.index == idx:
self.content_container.content = tab["content"]
self.page.update()
def fetch_and_update():
parts = url.split(":", 1)
if len(parts) != 2:
result = "Error: Invalid URL format. Expected format: hash:/page/path"
page_path = ""
else:
dest_hash = parts[0]
page_path = parts[1] if parts[1].startswith("/") else f"/{parts[1]}"
req = PageRequest(destination_hash=dest_hash, page_path=page_path)
page_fetcher = PageFetcher()
try:
result = page_fetcher.fetch_page(req)
except Exception as ex:
app_module.log_error(str(ex))
result = f"Error: {ex}"
try:
tab = self.manager.tabs[idx]
except IndexError:
return
if page_path and page_path.endswith(".mu"):
new_control = render_micron(result, on_link_click=handle_link_click)
else:
new_control = render_plaintext(result)
tab["content_control"] = new_control
tab["content"].controls[0] = new_control
if self.manager.index == idx:
self.content_container.content = tab["content"]
self.page.update()
self.page.run_thread(fetch_and_update)

View File

@@ -1,136 +1,493 @@
"""Settings interface for Ren Browser.
"""Settings interface for Ren Browser."""
Provides configuration management, log viewing, and storage
information display.
"""
from __future__ import annotations
from datetime import datetime
from pathlib import Path
import logging
import flet as ft
import RNS
from ren_browser.logs import ERROR_LOGS, RET_LOGS
from ren_browser import rns
from ren_browser.storage.storage import get_storage_manager
BUTTON_BG = "#0B3D91"
BUTTON_BG_HOVER = "#082C6C"
logger = logging.getLogger(__name__)
def _blue_button_style() -> ft.ButtonStyle:
return ft.ButtonStyle(
bgcolor=BUTTON_BG,
color=ft.Colors.WHITE,
overlay_color=BUTTON_BG_HOVER,
)
def _get_config_file_path() -> Path:
config_dir = rns.get_config_path()
if config_dir:
return Path(config_dir) / "config"
return Path.home() / ".reticulum" / "config"
def _read_config_text(config_path: Path) -> str:
try:
return config_path.read_text(encoding="utf-8")
except FileNotFoundError:
config_path.parent.mkdir(parents=True, exist_ok=True)
config_path.write_text("", encoding="utf-8")
return ""
except Exception as exc: # noqa: BLE001
return f"# Error loading config: {exc}"
def _write_config_text(config_path: Path, content: str) -> None:
config_path.parent.mkdir(parents=True, exist_ok=True)
config_path.write_text(content, encoding="utf-8")
def _get_interface_statuses():
statuses = []
interfaces = getattr(RNS.Transport, "interfaces", []) or []
for interface in interfaces:
if interface is None:
continue
if interface.__class__.__name__ == "LocalClientInterface" and getattr(
interface, "is_connected_to_shared_instance", False,
):
continue
statuses.append(
{
"name": getattr(interface, "name", None)
or interface.__class__.__name__,
"online": bool(getattr(interface, "online", False)),
"type": interface.__class__.__name__,
"bitrate": getattr(interface, "bitrate", None),
},
)
return statuses
def _format_bitrate(bitrate: int | None) -> str | None:
if not bitrate:
return None
if bitrate >= 1_000_000:
return f"{bitrate / 1_000_000:.1f} Mbps"
if bitrate >= 1_000:
return f"{bitrate / 1_000:.0f} kbps"
return f"{bitrate} bps"
def _build_interface_chip_controls(statuses):
if not statuses:
return [
ft.Text(
"No interfaces detected",
size=11,
color=ft.Colors.ON_SURFACE_VARIANT,
),
]
chips = []
for status in statuses:
indicator_color = ft.Colors.GREEN if status["online"] else ft.Colors.ERROR
tooltip = status["type"]
bitrate_label = _format_bitrate(status.get("bitrate"))
if bitrate_label:
tooltip = f"{tooltip} • {bitrate_label}"
chips.append(
ft.Container(
content=ft.Row(
[
ft.Icon(ft.Icons.CIRCLE, size=10, color=indicator_color),
ft.Text(status["name"], size=11),
],
spacing=4,
vertical_alignment=ft.CrossAxisAlignment.CENTER,
),
bgcolor="#1C1F2B",
border_radius=999,
padding=ft.padding.symmetric(horizontal=10, vertical=4),
tooltip=tooltip,
),
)
return chips
def _refresh_interface_status(summary_text, chip_wrap, updated_text):
statuses = _get_interface_statuses()
total = len(statuses)
online = sum(1 for entry in statuses if entry["online"])
if total == 0:
summary_text.value = "No active interfaces"
summary_text.color = ft.Colors.ERROR
else:
summary_text.value = f"{online}/{total} interfaces online"
summary_text.color = ft.Colors.GREEN if online else ft.Colors.ERROR
chip_wrap.controls = _build_interface_chip_controls(statuses)
updated_text.value = f"Updated {datetime.now().strftime('%H:%M:%S')}"
def _build_status_section(page: ft.Page):
summary_text = ft.Text("", size=16, weight=ft.FontWeight.BOLD)
updated_text = ft.Text("", size=12, color=ft.Colors.ON_SURFACE_VARIANT)
chip_wrap = ft.Row(
spacing=6,
run_spacing=6,
vertical_alignment=ft.CrossAxisAlignment.CENTER,
)
def refresh(_=None):
_refresh_interface_status(summary_text, chip_wrap, updated_text)
page.update()
refresh()
refresh_button = ft.IconButton(
icon=ft.Icons.REFRESH,
tooltip="Refresh status",
on_click=refresh,
icon_color=ft.Colors.BLUE_200,
)
section = ft.Column(
spacing=12,
controls=[
ft.Row(
controls=[
ft.Row(
controls=[
ft.Icon(ft.Icons.LAN, size=18, color=ft.Colors.BLUE_200),
summary_text,
],
spacing=6,
vertical_alignment=ft.CrossAxisAlignment.CENTER,
),
refresh_button,
],
alignment=ft.MainAxisAlignment.SPACE_BETWEEN,
vertical_alignment=ft.CrossAxisAlignment.CENTER,
),
chip_wrap,
updated_text,
],
)
return section, refresh
def _build_storage_field(storage):
storage_field = ft.TextField(
label="Storage Information",
value="",
expand=True,
multiline=True,
read_only=True,
min_lines=10,
max_lines=15,
border_color=ft.Colors.GREY_700,
text_style=ft.TextStyle(font_family="monospace", size=12),
)
def refresh():
info = storage.get_storage_info()
storage_field.value = "\n".join(
f"{key}: {value}" for key, value in info.items()
)
refresh()
return storage_field, refresh
def open_settings_tab(page: ft.Page, tab_manager):
"""Open a settings tab with configuration and debugging options.
Args:
page: Flet page instance for UI updates.
tab_manager: Tab manager to add the settings tab to.
"""
"""Open a settings tab with configuration, status, and storage info."""
storage = get_storage_manager(page)
try:
config_text = storage.load_config()
except Exception as ex:
config_text = f"Error reading config: {ex}"
config_path = _get_config_file_path()
config_text = _read_config_text(config_path)
app_settings = storage.load_app_settings()
config_field = ft.TextField(
label="Reticulum config",
label="Reticulum Configuration",
value=config_text,
expand=True,
multiline=True,
min_lines=15,
max_lines=20,
border_color=ft.Colors.GREY_700,
focused_border_color=ft.Colors.BLUE_400,
text_style=ft.TextStyle(font_family="monospace", size=12),
)
def on_save_config(ev):
horizontal_scroll_switch = ft.Switch(
label="Enable Horizontal Scroll (preserve ASCII art)",
value=app_settings.get("horizontal_scroll", False),
)
page_bgcolor_field = ft.TextField(
label="Page Background Color (hex)",
value=app_settings.get("page_bgcolor", "#000000"),
hint_text="#000000",
width=200,
border_color=ft.Colors.GREY_700,
focused_border_color=ft.Colors.BLUE_400,
)
color_preview = ft.Container(
width=40,
height=40,
bgcolor=app_settings.get("page_bgcolor", "#000000"),
border_radius=8,
border=ft.border.all(1, ft.Colors.GREY_700),
)
def on_bgcolor_change(_):
try:
success = storage.save_config(config_field.value)
color_preview.bgcolor = page_bgcolor_field.value
page.update()
except Exception as exc:
logger.warning(
"Ignoring invalid background color '%s': %s",
page_bgcolor_field.value,
exc,
)
page_bgcolor_field.on_change = on_bgcolor_change
def show_snack(message, *, success=True):
snack = ft.SnackBar(
content=ft.Row(
controls=[
ft.Icon(
ft.Icons.CHECK_CIRCLE if success else ft.Icons.ERROR,
color=ft.Colors.GREEN_400 if success else ft.Colors.RED_400,
size=20,
),
ft.Text(message, color=ft.Colors.WHITE),
],
tight=True,
),
bgcolor=ft.Colors.GREEN_900 if success else ft.Colors.RED_900,
duration=3000 if success else 4000,
)
page.overlay.append(snack)
snack.open = True
page.update()
def on_save_config(_):
try:
_write_config_text(config_path, config_field.value)
show_snack(f"Configuration saved to {config_path}")
except Exception as exc: # noqa: BLE001
show_snack(f"Failed to save configuration: {exc}", success=False)
def on_save_and_reload_config(_):
try:
_write_config_text(config_path, config_field.value)
except Exception as exc: # noqa: BLE001
show_snack(f"Failed to save configuration: {exc}", success=False)
return
loading_snack = ft.SnackBar(
content=ft.Row(
controls=[
ft.ProgressRing(
width=16,
height=16,
stroke_width=2,
color=ft.Colors.BLUE_400,
),
ft.Text("Reloading Reticulum...", color=ft.Colors.WHITE),
],
tight=True,
),
bgcolor=ft.Colors.BLUE_900,
duration=10000,
)
page.overlay.append(loading_snack)
loading_snack.open = True
page.update()
async def do_reload():
import ren_browser.app as app_module
try:
await app_module.reload_reticulum(page, on_reload_complete)
except Exception as exc: # noqa: BLE001
loading_snack.open = False
page.update()
show_snack(f"Reload failed: {exc}", success=False)
def on_reload_complete(success, error):
loading_snack.open = False
page.update()
if success:
print("Config saved successfully. Please restart the app.")
page.snack_bar = ft.SnackBar(
ft.Text("Config saved successfully. Please restart the app."),
open=True,
)
show_snack("Reticulum reloaded successfully!")
else:
print("Error saving config: Storage operation failed")
page.snack_bar = ft.SnackBar(
ft.Text("Error saving config: Storage operation failed"), open=True
show_snack(f"Reload failed: {error}", success=False)
page.run_task(do_reload)
def on_save_app_settings(_):
try:
new_settings = {
"horizontal_scroll": horizontal_scroll_switch.value,
"page_bgcolor": page_bgcolor_field.value,
}
success = storage.save_app_settings(new_settings)
if success:
if hasattr(tab_manager, "apply_settings"):
tab_manager.apply_settings(new_settings)
show_snack("Appearance settings saved and applied!")
else:
show_snack("Failed to save appearance settings", success=False)
except Exception as exc: # noqa: BLE001
show_snack(f"Error saving appearance: {exc}", success=False)
save_btn = ft.ElevatedButton(
"Save Configuration",
icon=ft.Icons.SAVE,
on_click=on_save_config,
style=_blue_button_style(),
)
except Exception as ex:
print(f"Error saving config: {ex}")
page.snack_bar = ft.SnackBar(
ft.Text(f"Error saving config: {ex}"), open=True
save_reload_btn = ft.ElevatedButton(
"Save & Hot Reload",
icon=ft.Icons.REFRESH,
on_click=on_save_and_reload_config,
style=_blue_button_style(),
)
save_appearance_btn = ft.ElevatedButton(
"Save Appearance",
icon=ft.Icons.PALETTE,
on_click=on_save_app_settings,
style=_blue_button_style(),
)
save_btn = ft.ElevatedButton("Save Config", on_click=on_save_config)
error_field = ft.TextField(
label="Error Logs",
value="",
expand=True,
multiline=True,
read_only=True,
)
ret_field = ft.TextField(
label="Reticulum logs",
value="",
expand=True,
multiline=True,
read_only=True,
status_content, refresh_status_section = _build_status_section(page)
storage_field, refresh_storage_info = _build_storage_field(storage)
appearance_content = ft.Column(
spacing=16,
controls=[
ft.Text("Appearance Settings", size=18, weight=ft.FontWeight.BOLD),
horizontal_scroll_switch,
ft.Row(
controls=[page_bgcolor_field, color_preview],
alignment=ft.MainAxisAlignment.START,
spacing=16,
),
save_appearance_btn,
],
)
# Storage information for debugging
storage_info = storage.get_storage_info()
storage_text = "\n".join([f"{key}: {value}" for key, value in storage_info.items()])
storage_field = ft.TextField(
label="Storage Information",
value=storage_text,
expand=True,
multiline=True,
read_only=True,
)
content_placeholder = ft.Container(expand=True, content=config_field)
content_placeholder = ft.Container(expand=True)
def show_config(ev):
def show_config(_):
content_placeholder.content = config_field
page.update()
def show_errors(ev):
error_field.value = "\n".join(ERROR_LOGS) or "No errors logged."
content_placeholder.content = error_field
def show_appearance(_):
content_placeholder.content = appearance_content
page.update()
def show_ret_logs(ev):
ret_field.value = "\n".join(RET_LOGS) or "No Reticulum logs."
content_placeholder.content = ret_field
page.update()
def show_status(_):
content_placeholder.content = status_content
refresh_status_section()
def show_storage_info(ev):
storage_info = storage.get_storage_info()
storage_field.value = "\n".join(
[f"{key}: {value}" for key, value in storage_info.items()]
)
def show_storage_info(_):
refresh_storage_info()
content_placeholder.content = storage_field
page.update()
def refresh_current_view(ev):
# Refresh the currently displayed content
if content_placeholder.content == error_field:
show_errors(ev)
elif content_placeholder.content == ret_field:
show_ret_logs(ev)
def refresh_current_view(_):
if content_placeholder.content == status_content:
refresh_status_section()
elif content_placeholder.content == storage_field:
show_storage_info(ev)
elif content_placeholder.content == config_field:
show_config(ev)
refresh_storage_info()
page.update()
btn_config = ft.ElevatedButton("Config", on_click=show_config)
btn_errors = ft.ElevatedButton("Errors", on_click=show_errors)
btn_ret = ft.ElevatedButton("Ret Logs", on_click=show_ret_logs)
btn_storage = ft.ElevatedButton("Storage", on_click=show_storage_info)
btn_refresh = ft.ElevatedButton("Refresh", on_click=refresh_current_view)
button_row = ft.Row(
controls=[btn_config, btn_errors, btn_ret, btn_storage, btn_refresh]
btn_config = ft.FilledButton(
"Configuration",
icon=ft.Icons.SETTINGS,
on_click=show_config,
style=_blue_button_style(),
)
content_placeholder.content = config_field
btn_appearance = ft.FilledButton(
"Appearance",
icon=ft.Icons.PALETTE,
on_click=show_appearance,
style=_blue_button_style(),
)
btn_status = ft.FilledButton(
"Status",
icon=ft.Icons.LAN,
on_click=show_status,
style=_blue_button_style(),
)
btn_storage = ft.FilledButton(
"Storage",
icon=ft.Icons.STORAGE,
on_click=show_storage_info,
style=_blue_button_style(),
)
btn_refresh = ft.IconButton(
icon=ft.Icons.REFRESH,
tooltip="Refresh",
on_click=refresh_current_view,
icon_color=ft.Colors.BLUE_400,
)
nav_card = ft.Container(
content=ft.Row(
controls=[btn_config, btn_appearance, btn_status, btn_storage, btn_refresh],
spacing=8,
wrap=True,
),
padding=ft.padding.all(16),
border_radius=12,
bgcolor=ft.Colors.GREY_900,
)
content_card = ft.Container(
content=content_placeholder,
expand=True,
padding=ft.padding.all(16),
border_radius=12,
bgcolor=ft.Colors.GREY_900,
)
action_row = ft.Container(
content=ft.Row(
controls=[save_btn, save_reload_btn],
alignment=ft.MainAxisAlignment.END,
spacing=8,
),
padding=ft.padding.symmetric(horizontal=16, vertical=8),
)
settings_content = ft.Column(
expand=True,
spacing=16,
controls=[
button_row,
content_placeholder,
ft.Row([save_btn]),
ft.Container(
content=ft.Text(
"Settings",
size=24,
weight=ft.FontWeight.BOLD,
color=ft.Colors.BLUE_400,
),
padding=ft.padding.only(left=16, top=16),
),
nav_card,
content_card,
action_row,
],
)
tab_manager._add_tab_internal("Settings", settings_content)
idx = len(tab_manager.manager.tabs) - 1
tab_manager.select_tab(idx)

View File

@@ -23,11 +23,26 @@ def build_ui(page: Page):
"""
page.theme_mode = ft.ThemeMode.DARK
page.appbar = ft.AppBar()
page.theme = ft.Theme(
color_scheme=ft.ColorScheme(
primary=ft.Colors.BLUE_400,
on_primary=ft.Colors.WHITE,
surface=ft.Colors.BLACK,
on_surface=ft.Colors.WHITE,
background=ft.Colors.BLACK,
on_background=ft.Colors.WHITE,
),
)
page.bgcolor = ft.Colors.BLACK
page.appbar = ft.AppBar(
bgcolor=ft.Colors.GREY_900,
elevation=2,
)
page.window.maximized = True
page.padding = 0
page_fetcher = PageFetcher()
announce_list = ft.ListView(expand=True, spacing=1)
announce_list = ft.ListView(expand=True, spacing=8, padding=ft.padding.all(8))
def update_announces(ann_list):
announce_list.controls.clear()
@@ -58,8 +73,21 @@ def build_ui(page: Page):
tab = tab_manager.manager.tabs[idx]
except IndexError:
return
def handle_link_click(url):
full_url = url
if ":" not in url:
full_url = f"{url}:/page/index.mu"
elif url.startswith(":/"):
full_url = f"{dest}{url}"
tab["url_field"].value = full_url
tab_manager._on_tab_go(None, idx)
if req.page_path.endswith(".mu"):
new_control = render_micron(result)
new_control = render_micron(
result,
on_link_click=handle_link_click,
)
else:
new_control = render_plaintext(result)
tab["content_control"] = new_control
@@ -70,25 +98,50 @@ def build_ui(page: Page):
page.run_thread(fetch_and_update)
announce_list.controls.append(ft.TextButton(label, on_click=on_click_ann))
announce_card = ft.Container(
content=ft.Row(
controls=[
ft.Icon(ft.Icons.LANGUAGE, size=20, color=ft.Colors.BLUE_400),
ft.Text(
label,
size=14,
weight=ft.FontWeight.W_500,
overflow=ft.TextOverflow.ELLIPSIS,
),
],
spacing=12,
),
padding=ft.padding.all(12),
border_radius=8,
bgcolor=ft.Colors.GREY_800,
ink=True,
on_click=on_click_ann,
)
announce_list.controls.append(announce_card)
page.update()
AnnounceService(update_callback=update_announces)
page.drawer = ft.NavigationDrawer(
bgcolor=ft.Colors.GREY_900,
elevation=8,
controls=[
ft.Text(
ft.Container(
content=ft.Text(
"Announcements",
size=20,
weight=ft.FontWeight.BOLD,
text_align=ft.TextAlign.CENTER,
expand=True,
color=ft.Colors.BLUE_400,
),
ft.Divider(),
padding=ft.padding.symmetric(horizontal=16, vertical=20),
),
ft.Divider(height=1, color=ft.Colors.GREY_700),
announce_list,
],
)
page.appbar.leading = ft.IconButton(
ft.Icons.MENU,
tooltip="Toggle sidebar",
tooltip="Announcements",
icon_color=ft.Colors.WHITE,
on_click=lambda e: (
setattr(page.drawer, "open", not page.drawer.open),
page.update(),
@@ -102,15 +155,21 @@ def build_ui(page: Page):
ft.IconButton(
ft.Icons.SETTINGS,
tooltip="Settings",
icon_color=ft.Colors.WHITE,
on_click=lambda e: open_settings_tab(page, tab_manager),
)
),
]
Shortcuts(page, tab_manager)
url_bar = ft.Row(
url_bar = ft.Container(
content=ft.Row(
controls=[
tab_manager.manager.tabs[tab_manager.manager.index]["url_field"],
tab_manager.manager.tabs[tab_manager.manager.index]["go_btn"],
],
spacing=8,
),
expand=True,
padding=ft.padding.symmetric(horizontal=8),
)
page.appbar.title = url_bar
orig_select_tab = tab_manager.select_tab
@@ -118,8 +177,8 @@ def build_ui(page: Page):
def _select_tab_and_update_url(i):
orig_select_tab(i)
tab = tab_manager.manager.tabs[i]
url_bar.controls.clear()
url_bar.controls.extend([tab["url_field"], tab["go_btn"]])
url_bar.content.controls.clear()
url_bar.content.controls.extend([tab["url_field"], tab["go_btn"]])
page.update()
tab_manager.select_tab = _select_tab_and_update_url

View File

@@ -62,7 +62,9 @@ def sample_page_request():
from ren_browser.pages.page_request import PageRequest
return PageRequest(
destination_hash="1234567890abcdef", page_path="/page/index.mu", field_data=None
destination_hash="1234567890abcdef",
page_path="/page/index.mu",
field_data=None,
)
@@ -74,6 +76,11 @@ def mock_storage_manager():
mock_storage.save_config.return_value = True
mock_storage.get_config_path.return_value = Mock()
mock_storage.get_reticulum_config_path.return_value = Mock()
mock_storage.load_app_settings.return_value = {
"horizontal_scroll": False,
"page_bgcolor": "#000000",
}
mock_storage.save_app_settings.return_value = True
mock_storage.get_storage_info.return_value = {
"storage_dir": "/mock/storage",
"config_path": "/mock/storage/config.txt",

View File

@@ -1,5 +1,6 @@
from unittest.mock import Mock
import flet as ft
import pytest
from ren_browser import app
@@ -14,16 +15,21 @@ class TestAppIntegration:
mock_page = Mock()
mock_page.add = Mock()
mock_page.update = Mock()
mock_page.run_thread = Mock()
mock_page.controls = Mock()
mock_page.controls.clear = Mock()
mock_page.width = 1024
mock_page.window = Mock()
mock_page.window.maximized = False
mock_page.appbar = Mock()
mock_page.drawer = Mock()
mock_page.theme_mode = ft.ThemeMode.DARK
await app.main(mock_page)
# Verify that the main function sets up the loading screen
mock_page.add.assert_called_once()
assert mock_page.add.call_count >= 1
loader_call = mock_page.add.call_args_list[0][0][0]
assert isinstance(loader_call, ft.Container)
mock_page.update.assert_called()
mock_page.run_thread.assert_called_once()
def test_entry_points_exist(self):
"""Test that all expected entry points exist and are callable."""

View File

@@ -19,7 +19,9 @@ class TestAnnounce:
def test_announce_with_none_display_name(self):
"""Test Announce creation with None display name."""
announce = Announce(
destination_hash="1234567890abcdef", display_name=None, timestamp=1234567890
destination_hash="1234567890abcdef",
display_name=None,
timestamp=1234567890,
)
assert announce.destination_hash == "1234567890abcdef"

View File

@@ -12,26 +12,34 @@ class TestApp:
@pytest.mark.asyncio
async def test_main_initializes_loader(self, mock_page, mock_rns):
"""Test that main function initializes with loading screen."""
with patch("ren_browser.ui.ui.build_ui"):
with (
patch("ren_browser.rns.initialize_reticulum", return_value=True),
patch("ren_browser.rns.get_reticulum_instance"),
patch("ren_browser.rns.get_config_path", return_value="/tmp/.reticulum"),
patch("ren_browser.app.build_ui"),
):
await app.main(mock_page)
mock_page.add.assert_called_once()
assert mock_page.add.call_count >= 1
loader_call = mock_page.add.call_args_list[0][0][0]
assert isinstance(loader_call, ft.Container)
mock_page.update.assert_called()
mock_page.run_thread.assert_called_once()
@pytest.mark.asyncio
async def test_main_function_structure(self, mock_page, mock_rns):
"""Test that main function sets up the expected structure."""
with (
patch("ren_browser.rns.initialize_reticulum", return_value=True),
patch("ren_browser.rns.get_reticulum_instance"),
patch("ren_browser.rns.get_config_path"),
patch("ren_browser.app.build_ui"),
):
await app.main(mock_page)
# Verify that main function adds content and sets up threading
mock_page.add.assert_called_once()
assert mock_page.add.call_count >= 1
loader_call = mock_page.add.call_args_list[0][0][0]
assert isinstance(loader_call, ft.Container)
mock_page.update.assert_called()
mock_page.run_thread.assert_called_once()
# Verify that a function was passed to run_thread
init_function = mock_page.run_thread.call_args[0][0]
assert callable(init_function)
def test_run_with_default_args(self, mock_rns):
"""Test run function with default arguments."""

View File

@@ -59,7 +59,9 @@ class TestLogsModule:
assert len(logs.RET_LOGS) == 1
assert logs.RET_LOGS[0] == "[2023-01-01T12:00:00] Test RNS message"
logs._original_rns_log.assert_called_once_with(
"Test RNS message", "arg1", kwarg1="value1"
"Test RNS message",
"arg1",
kwarg1="value1",
)
assert result == "original_result"

View File

@@ -7,7 +7,8 @@ class TestPageRequest:
def test_page_request_creation(self):
"""Test basic PageRequest creation."""
request = PageRequest(
destination_hash="1234567890abcdef", page_path="/page/index.mu"
destination_hash="1234567890abcdef",
page_path="/page/index.mu",
)
assert request.destination_hash == "1234567890abcdef"

View File

@@ -63,66 +63,58 @@ class TestMicronRenderer:
"""
def test_render_micron_basic(self):
"""Test basic micron rendering (currently displays raw content)."""
"""Test basic micron rendering."""
content = "# Heading\n\nSome content"
result = render_micron(content)
assert isinstance(result, ft.Text)
assert result.value == "# Heading\n\nSome content"
assert result.selectable is True
assert result.font_family == "monospace"
assert isinstance(result, ft.Column)
assert result.expand is True
assert result.scroll == ft.ScrollMode.AUTO
def test_render_micron_empty(self):
"""Test micron rendering with empty content."""
content = ""
result = render_micron(content)
assert isinstance(result, ft.Text)
assert result.value == ""
assert result.selectable is True
assert isinstance(result, ft.Column)
assert len(result.controls) >= 0
def test_render_micron_unicode(self):
"""Test micron rendering with Unicode characters."""
content = "Unicode content: 你好 🌍 αβγ"
result = render_micron(content)
assert isinstance(result, ft.Text)
assert result.value == content
assert result.selectable is True
assert isinstance(result, ft.Column)
assert len(result.controls) > 0
class TestRendererComparison:
"""Test cases comparing both renderers."""
def test_renderers_return_same_type(self):
"""Test that both renderers return the same control type."""
"""Test that both renderers return Flet controls."""
content = "Test content"
plaintext_result = render_plaintext(content)
micron_result = render_micron(content)
assert type(plaintext_result) is type(micron_result)
assert isinstance(plaintext_result, ft.Text)
assert isinstance(micron_result, ft.Text)
assert isinstance(micron_result, ft.Column)
def test_renderers_preserve_content(self):
"""Test that both renderers preserve the original content."""
"""Test that plaintext renderer preserves content."""
content = "Test content with\nmultiple lines"
plaintext_result = render_plaintext(content)
micron_result = render_micron(content)
assert plaintext_result.value == content
assert micron_result.value == content
def test_renderers_same_properties(self):
"""Test that both renderers set the same basic properties."""
"""Test that both renderers have expand property."""
content = "Test content"
plaintext_result = render_plaintext(content)
micron_result = render_micron(content)
assert plaintext_result.selectable == micron_result.selectable
assert plaintext_result.font_family == micron_result.font_family
assert plaintext_result.expand == micron_result.expand
assert plaintext_result.expand is True
assert micron_result.expand is True

View File

@@ -18,7 +18,7 @@ class TestStorageManager:
def test_storage_manager_init_without_page(self):
"""Test StorageManager initialization without a page."""
with patch(
"ren_browser.storage.storage.StorageManager._get_storage_directory"
"ren_browser.storage.storage.StorageManager._get_storage_directory",
) as mock_get_dir:
mock_dir = Path("/mock/storage")
mock_get_dir.return_value = mock_dir
@@ -35,7 +35,7 @@ class TestStorageManager:
mock_page = Mock()
with patch(
"ren_browser.storage.storage.StorageManager._get_storage_directory"
"ren_browser.storage.storage.StorageManager._get_storage_directory",
) as mock_get_dir:
mock_dir = Path("/mock/storage")
mock_get_dir.return_value = mock_dir
@@ -51,12 +51,14 @@ class TestStorageManager:
with (
patch("os.name", "posix"),
patch.dict(
"os.environ", {"XDG_CONFIG_HOME": "/home/user/.config"}, clear=True
"os.environ",
{"XDG_CONFIG_HOME": "/home/user/.config"},
clear=True,
),
patch("pathlib.Path.mkdir"),
):
with patch(
"ren_browser.storage.storage.StorageManager._ensure_storage_directory"
patch(
"ren_browser.storage.storage.StorageManager._ensure_storage_directory",
),
):
storage = StorageManager()
storage._storage_dir = storage._get_storage_directory()
@@ -68,19 +70,57 @@ class TestStorageManager:
# Skip this test on non-Windows systems to avoid path issues
pytest.skip("Windows path test skipped on non-Windows system")
def test_get_storage_directory_android(self):
"""Test storage directory detection for Android."""
def test_get_storage_directory_android_with_android_data(self):
"""Test storage directory detection for Android with ANDROID_DATA."""
with (
patch("os.name", "posix"),
patch.dict(
"os.environ",
{"ANDROID_ROOT": "/system", "ANDROID_DATA": "/data"},
clear=True,
),
patch("pathlib.Path.mkdir"),
patch(
"ren_browser.storage.storage.StorageManager._ensure_storage_directory",
),
):
storage = StorageManager()
storage._storage_dir = storage._get_storage_directory()
expected_dir = Path("/data/ren_browser")
assert storage._storage_dir == expected_dir
def test_get_storage_directory_android_with_external_storage(self):
"""Test storage directory detection for Android with EXTERNAL_STORAGE."""
with (
patch("os.name", "posix"),
patch.dict(
"os.environ",
{"ANDROID_ROOT": "/system", "EXTERNAL_STORAGE": "/storage/emulated/0"},
clear=True,
),
patch("pathlib.Path.mkdir"),
patch(
"ren_browser.storage.storage.StorageManager._ensure_storage_directory",
),
):
storage = StorageManager()
storage._storage_dir = storage._get_storage_directory()
expected_dir = Path("/storage/emulated/0/ren_browser")
assert storage._storage_dir == expected_dir
def test_get_storage_directory_android_fallback(self):
"""Test storage directory detection for Android with fallback."""
with (
patch("os.name", "posix"),
patch.dict("os.environ", {"ANDROID_ROOT": "/system"}, clear=True),
patch("pathlib.Path.mkdir"),
):
with patch(
"ren_browser.storage.storage.StorageManager._ensure_storage_directory"
patch(
"ren_browser.storage.storage.StorageManager._ensure_storage_directory",
),
):
storage = StorageManager()
storage._storage_dir = storage._get_storage_directory()
expected_dir = Path("/storage/emulated/0/Documents/ren_browser")
expected_dir = Path("/data/local/tmp/ren_browser")
assert storage._storage_dir == expected_dir
def test_get_config_path(self):
@@ -141,7 +181,8 @@ class TestStorageManager:
assert result is True
mock_page.client_storage.set.assert_called_with(
"ren_browser_config", config_content
"ren_browser_config",
config_content,
)
def test_save_config_fallback(self):
@@ -154,14 +195,16 @@ class TestStorageManager:
storage._storage_dir = Path(temp_dir)
# Mock the reticulum config path to use temp dir and cause failure
with patch.object(
with (
patch.object(
storage,
"get_reticulum_config_path",
return_value=Path(temp_dir) / "reticulum",
):
with patch(
),
patch(
"pathlib.Path.write_text",
side_effect=PermissionError("Access denied"),
),
):
config_content = "test config content"
result = storage.save_config(config_content)
@@ -169,7 +212,8 @@ class TestStorageManager:
assert result is True
# Check that the config was set to client storage
mock_page.client_storage.set.assert_any_call(
"ren_browser_config", config_content
"ren_browser_config",
config_content,
)
# Verify that client storage was called at least once
assert mock_page.client_storage.set.call_count >= 1
@@ -240,7 +284,7 @@ class TestStorageManager:
bookmarks_path = storage._storage_dir / "bookmarks.json"
assert bookmarks_path.exists()
with open(bookmarks_path, "r", encoding="utf-8") as f:
with open(bookmarks_path, encoding="utf-8") as f:
loaded_bookmarks = json.load(f)
assert loaded_bookmarks == bookmarks
@@ -281,7 +325,7 @@ class TestStorageManager:
history_path = storage._storage_dir / "history.json"
assert history_path.exists()
with open(history_path, "r", encoding="utf-8") as f:
with open(history_path, encoding="utf-8") as f:
loaded_history = json.load(f)
assert loaded_history == history
@@ -327,11 +371,13 @@ class TestStorageManager:
with patch.object(StorageManager, "_get_storage_directory") as mock_get_dir:
mock_get_dir.return_value = Path("/nonexistent/path")
with patch(
with (
patch(
"pathlib.Path.mkdir",
side_effect=[PermissionError("Access denied"), None],
),
patch("tempfile.gettempdir", return_value="/tmp"),
):
with patch("tempfile.gettempdir", return_value="/tmp"):
storage = StorageManager()
expected_fallback = Path("/tmp") / "ren_browser"
@@ -418,7 +464,8 @@ class TestStorageManagerEdgeCases:
storage = StorageManager()
with patch(
"pathlib.Path.write_text", side_effect=PermissionError("Access denied")
"pathlib.Path.write_text",
side_effect=PermissionError("Access denied"),
):
test_path = Path("/mock/path")
result = storage._is_writable(test_path)

View File

@@ -34,8 +34,8 @@ class TestTabsManager:
assert isinstance(manager.manager, SimpleNamespace)
assert len(manager.manager.tabs) == 1
assert manager.manager.index == 0
assert isinstance(manager.tab_bar, ft.Row)
assert manager.tab_bar.scroll is None
assert isinstance(manager.tab_bar, ft.Container)
assert isinstance(manager.tab_bar.content, ft.Row)
assert manager.overflow_menu is None
assert isinstance(manager.content_container, ft.Container)
@@ -105,12 +105,14 @@ class TestTabsManager:
"""Test that selecting a tab updates background colors correctly."""
tabs_manager._add_tab_internal("Tab 2", Mock())
tab_controls = tabs_manager.tab_bar.controls[:-2] # Exclude add/close buttons
tab_controls = tabs_manager.tab_bar.content.controls[
:-2
] # Exclude add/close buttons
tabs_manager.select_tab(1)
assert tab_controls[0].bgcolor == ft.Colors.SURFACE_CONTAINER_HIGHEST
assert tab_controls[1].bgcolor == ft.Colors.PRIMARY_CONTAINER
assert tab_controls[0].bgcolor == ft.Colors.GREY_800
assert tab_controls[1].bgcolor == ft.Colors.BLUE_900
def test_on_tab_go_empty_url(self, tabs_manager):
"""Test tab go with empty URL."""
@@ -146,12 +148,12 @@ class TestTabsManager:
def test_tab_container_properties(self, tabs_manager):
"""Test that tab container has correct properties."""
assert tabs_manager.content_container.expand is True
assert tabs_manager.content_container.bgcolor == ft.Colors.BLACK
assert tabs_manager.content_container.padding == ft.padding.all(5)
assert tabs_manager.content_container.bgcolor in (ft.Colors.BLACK, "#000000")
assert tabs_manager.content_container.padding == ft.padding.all(16)
def test_tab_bar_controls(self, tabs_manager):
"""Test that tab bar has correct controls."""
controls = tabs_manager.tab_bar.controls
controls = tabs_manager.tab_bar.content.controls
# Should have: home tab, add button, close button (and potentially overflow menu)
assert len(controls) >= 3
@@ -180,7 +182,7 @@ class TestTabsManager:
url_field = tab["url_field"]
assert url_field.expand is True
assert url_field.text_style.size == 12
assert url_field.text_style.size == 14
assert url_field.content_padding is not None
def test_go_button_properties(self, tabs_manager):
@@ -188,14 +190,16 @@ class TestTabsManager:
tab = tabs_manager.manager.tabs[0]
go_btn = tab["go_btn"]
assert go_btn.icon == ft.Icons.OPEN_IN_BROWSER
assert go_btn.tooltip == "Load URL"
assert go_btn.icon == ft.Icons.ARROW_FORWARD
assert go_btn.tooltip == "Go"
def test_tab_click_handlers(self, tabs_manager):
"""Test that tab click handlers are properly set."""
tabs_manager._add_tab_internal("Tab 2", Mock())
tab_controls = tabs_manager.tab_bar.controls[:-2] # Exclude add/close buttons
tab_controls = tabs_manager.tab_bar.content.controls[
:-2
] # Exclude add/close buttons
for i, control in enumerate(tab_controls):
assert control.on_click is not None
@@ -249,13 +253,21 @@ class TestTabsManager:
# Simulate a smaller screen, expecting more tabs to overflow
tabs_manager.page.width = 400
tabs_manager._update_tab_visibility()
visible_tabs_small = sum(1 for c in tabs_manager.tab_bar.controls if isinstance(c, ft.Container) and c.visible)
visible_tabs_small = sum(
1
for c in tabs_manager.tab_bar.content.controls
if isinstance(c, ft.Container) and c.visible
)
assert visible_tabs_small < 11
# Simulate a larger screen, expecting all tabs to be visible
tabs_manager.page.width = 1600
tabs_manager._update_tab_visibility()
visible_tabs_large = sum(1 for c in tabs_manager.tab_bar.controls if isinstance(c, ft.Container) and c.visible)
visible_tabs_large = sum(
1
for c in tabs_manager.tab_bar.content.controls
if isinstance(c, ft.Container) and c.visible
)
assert visible_tabs_large == 11
assert tabs_manager.overflow_menu is None

View File

@@ -29,7 +29,12 @@ class TestBuildUI:
@patch("ren_browser.tabs.tabs.TabsManager")
@patch("ren_browser.controls.shortcuts.Shortcuts")
def test_build_ui_appbar_setup(
self, mock_shortcuts, mock_tabs, mock_fetcher, mock_announce_service, mock_page
self,
mock_shortcuts,
mock_tabs,
mock_fetcher,
mock_announce_service,
mock_page,
):
"""Test that build_ui sets up the app bar correctly."""
mock_tab_manager = Mock()
@@ -51,7 +56,12 @@ class TestBuildUI:
@patch("ren_browser.tabs.tabs.TabsManager")
@patch("ren_browser.controls.shortcuts.Shortcuts")
def test_build_ui_drawer_setup(
self, mock_shortcuts, mock_tabs, mock_fetcher, mock_announce_service, mock_page
self,
mock_shortcuts,
mock_tabs,
mock_fetcher,
mock_announce_service,
mock_page,
):
"""Test that build_ui sets up the drawer correctly."""
mock_tab_manager = Mock()
@@ -83,28 +93,50 @@ class TestBuildUI:
class TestOpenSettingsTab:
"""Test cases for the open_settings_tab function."""
def test_open_settings_tab_basic(self, mock_page):
def test_open_settings_tab_basic(self, mock_page, mock_storage_manager):
"""Test opening settings tab with basic functionality."""
mock_tab_manager = Mock()
mock_tab_manager.manager.tabs = []
mock_tab_manager._add_tab_internal = Mock()
mock_tab_manager.select_tab = Mock()
with patch("pathlib.Path.read_text", return_value="config content"):
mock_page.overlay = []
with (
patch(
"ren_browser.ui.settings.get_storage_manager",
return_value=mock_storage_manager,
),
patch(
"ren_browser.ui.settings.rns.get_config_path", return_value="/tmp/rns",
),
patch("pathlib.Path.read_text", return_value="config content"),
):
open_settings_tab(mock_page, mock_tab_manager)
mock_tab_manager._add_tab_internal.assert_called_once()
mock_tab_manager.select_tab.assert_called_once()
mock_page.update.assert_called()
def test_open_settings_tab_config_error(self, mock_page):
def test_open_settings_tab_config_error(self, mock_page, mock_storage_manager):
"""Test opening settings tab when config file cannot be read."""
mock_tab_manager = Mock()
mock_tab_manager.manager.tabs = []
mock_tab_manager._add_tab_internal = Mock()
mock_tab_manager.select_tab = Mock()
with patch("pathlib.Path.read_text", side_effect=Exception("File not found")):
mock_page.overlay = []
with (
patch(
"ren_browser.ui.settings.get_storage_manager",
return_value=mock_storage_manager,
),
patch(
"ren_browser.ui.settings.rns.get_config_path", return_value="/tmp/rns",
),
patch("pathlib.Path.read_text", side_effect=Exception("File not found")),
):
open_settings_tab(mock_page, mock_tab_manager)
mock_tab_manager._add_tab_internal.assert_called_once()
@@ -113,69 +145,110 @@ class TestOpenSettingsTab:
args = mock_tab_manager._add_tab_internal.call_args
assert args[0][0] == "Settings"
def test_settings_save_config_success(self, mock_page):
def test_settings_save_config_success(self, mock_page, mock_storage_manager):
"""Test saving config successfully in settings."""
mock_tab_manager = Mock()
mock_tab_manager.manager.tabs = []
mock_tab_manager._add_tab_internal = Mock()
mock_tab_manager.select_tab = Mock()
with (
patch("pathlib.Path.read_text", return_value="config"),
patch("pathlib.Path.write_text"),
):
open_settings_tab(mock_page, mock_tab_manager)
# Get the settings content that was added
settings_content = mock_tab_manager._add_tab_internal.call_args[0][1]
# Find the save button and simulate click
save_btn = None
for control in settings_content.controls:
if hasattr(control, "controls"):
for sub_control in control.controls:
if (
hasattr(sub_control, "text")
and sub_control.text == "Save Config"
):
save_btn = sub_control
break
assert save_btn is not None
def test_settings_save_config_error(self, mock_page, mock_storage_manager):
"""Test saving config with error in settings."""
mock_tab_manager = Mock()
mock_tab_manager.manager.tabs = []
mock_tab_manager._add_tab_internal = Mock()
mock_tab_manager.select_tab = Mock()
with patch(
"ren_browser.ui.settings.get_storage_manager",
return_value=mock_storage_manager,
):
open_settings_tab(mock_page, mock_tab_manager)
settings_content = mock_tab_manager._add_tab_internal.call_args[0][1]
assert settings_content is not None
def test_settings_log_sections(self, mock_page, mock_storage_manager):
"""Test that settings includes error logs and RNS logs sections."""
mock_tab_manager = Mock()
mock_tab_manager.manager.tabs = []
mock_tab_manager._add_tab_internal = Mock()
mock_tab_manager.select_tab = Mock()
mock_page.overlay = []
with (
patch(
"ren_browser.ui.settings.get_storage_manager",
return_value=mock_storage_manager,
),
patch("ren_browser.logs.ERROR_LOGS", ["Error 1", "Error 2"]),
patch("ren_browser.logs.RET_LOGS", ["RNS log 1", "RNS log 2"]),
patch(
"ren_browser.ui.settings.rns.get_config_path", return_value="/tmp/rns",
),
patch("pathlib.Path.read_text", return_value="config"),
patch("pathlib.Path.write_text") as mock_write,
):
open_settings_tab(mock_page, mock_tab_manager)
mock_tab_manager._add_tab_internal.assert_called_once()
args = mock_tab_manager._add_tab_internal.call_args
assert args[0][0] == "Settings"
# Get the settings content that was added
settings_content = mock_tab_manager._add_tab_internal.call_args[0][1]
# Find the save button - now nested in action_row container
save_btn = None
for control in settings_content.controls:
if hasattr(control, "content") and hasattr(control.content, "controls"):
for sub_control in control.content.controls:
if (
hasattr(sub_control, "text")
and sub_control.text == "Save Configuration"
):
save_btn = sub_control
break
assert save_btn is not None
save_btn.on_click(None)
assert mock_write.called
def test_settings_save_config_error(self, mock_page, mock_storage_manager):
"""Test saving config error path does not crash."""
mock_tab_manager = Mock()
mock_tab_manager.manager.tabs = []
mock_tab_manager._add_tab_internal = Mock()
mock_tab_manager.select_tab = Mock()
mock_page.overlay = []
with (
patch(
"ren_browser.ui.settings.get_storage_manager",
return_value=mock_storage_manager,
),
patch(
"ren_browser.ui.settings.rns.get_config_path", return_value="/tmp/rns",
),
patch("pathlib.Path.read_text", return_value="config"),
patch("pathlib.Path.write_text", side_effect=Exception("disk full")),
):
open_settings_tab(mock_page, mock_tab_manager)
settings_content = mock_tab_manager._add_tab_internal.call_args[0][1]
save_btn = None
for control in settings_content.controls:
if hasattr(control, "content") and hasattr(control.content, "controls"):
for sub_control in control.content.controls:
if (
hasattr(sub_control, "text")
and sub_control.text == "Save Configuration"
):
save_btn = sub_control
break
assert save_btn is not None
# Should not raise despite write failure
save_btn.on_click(None)
def test_settings_status_section_present(self, mock_page, mock_storage_manager):
"""Ensure the status navigation button is present."""
mock_tab_manager = Mock()
mock_tab_manager.manager.tabs = []
mock_tab_manager._add_tab_internal = Mock()
mock_tab_manager.select_tab = Mock()
mock_page.overlay = []
with (
patch(
"ren_browser.ui.settings.get_storage_manager",
return_value=mock_storage_manager,
),
patch(
"ren_browser.ui.settings.rns.get_config_path", return_value="/tmp/rns",
),
patch("pathlib.Path.read_text", return_value="config"),
):
open_settings_tab(mock_page, mock_tab_manager)
settings_content = mock_tab_manager._add_tab_internal.call_args[0][1]
nav_container = settings_content.controls[1]
button_labels = [
ctrl.text
for ctrl in nav_container.content.controls
if hasattr(ctrl, "text")
]
assert "Status" in button_labels

327
uv.lock generated
View File

@@ -1,6 +1,6 @@
version = 1
revision = 3
requires-python = ">=3.13"
requires-python = ">=3.11"
[[package]]
name = "annotated-doc"
@@ -27,6 +27,7 @@ source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "idna" },
{ name = "sniffio" },
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/c6/78/7d432127c41b50bccba979505f272c16cbcadcc33645d5fa3a738110ae75/anyio-4.11.0.tar.gz", hash = "sha256:82a8d0b81e318cc5ce71a5f1f8b5c4e63619620b63141ef8c995fa0db95a57c4", size = 219094, upload-time = "2025-09-23T09:19:12.58Z" }
wheels = [
@@ -76,6 +77,31 @@ dependencies = [
]
sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" },
{ url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" },
{ url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" },
{ url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" },
{ url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" },
{ url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" },
{ url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" },
{ url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" },
{ url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" },
{ url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" },
{ url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" },
{ url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" },
{ url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" },
{ url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" },
{ url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" },
{ url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" },
{ url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" },
{ url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" },
{ url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" },
{ url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" },
{ url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" },
{ url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" },
{ url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" },
{ url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" },
{ url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" },
{ url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" },
{ url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" },
{ url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" },
@@ -127,6 +153,38 @@ version = "3.4.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/13/69/33ddede1939fdd074bce5434295f38fae7136463422fe4fd3e0e89b98062/charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a", size = 129418, upload-time = "2025-10-14T04:42:32.879Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ed/27/c6491ff4954e58a10f69ad90aca8a1b6fe9c5d3c6f380907af3c37435b59/charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8", size = 206988, upload-time = "2025-10-14T04:40:33.79Z" },
{ url = "https://files.pythonhosted.org/packages/94/59/2e87300fe67ab820b5428580a53cad894272dbb97f38a7a814a2a1ac1011/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0", size = 147324, upload-time = "2025-10-14T04:40:34.961Z" },
{ url = "https://files.pythonhosted.org/packages/07/fb/0cf61dc84b2b088391830f6274cb57c82e4da8bbc2efeac8c025edb88772/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3", size = 142742, upload-time = "2025-10-14T04:40:36.105Z" },
{ url = "https://files.pythonhosted.org/packages/62/8b/171935adf2312cd745d290ed93cf16cf0dfe320863ab7cbeeae1dcd6535f/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc", size = 160863, upload-time = "2025-10-14T04:40:37.188Z" },
{ url = "https://files.pythonhosted.org/packages/09/73/ad875b192bda14f2173bfc1bc9a55e009808484a4b256748d931b6948442/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897", size = 157837, upload-time = "2025-10-14T04:40:38.435Z" },
{ url = "https://files.pythonhosted.org/packages/6d/fc/de9cce525b2c5b94b47c70a4b4fb19f871b24995c728e957ee68ab1671ea/charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381", size = 151550, upload-time = "2025-10-14T04:40:40.053Z" },
{ url = "https://files.pythonhosted.org/packages/55/c2/43edd615fdfba8c6f2dfbd459b25a6b3b551f24ea21981e23fb768503ce1/charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815", size = 149162, upload-time = "2025-10-14T04:40:41.163Z" },
{ url = "https://files.pythonhosted.org/packages/03/86/bde4ad8b4d0e9429a4e82c1e8f5c659993a9a863ad62c7df05cf7b678d75/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0", size = 150019, upload-time = "2025-10-14T04:40:42.276Z" },
{ url = "https://files.pythonhosted.org/packages/1f/86/a151eb2af293a7e7bac3a739b81072585ce36ccfb4493039f49f1d3cae8c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161", size = 143310, upload-time = "2025-10-14T04:40:43.439Z" },
{ url = "https://files.pythonhosted.org/packages/b5/fe/43dae6144a7e07b87478fdfc4dbe9efd5defb0e7ec29f5f58a55aeef7bf7/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4", size = 162022, upload-time = "2025-10-14T04:40:44.547Z" },
{ url = "https://files.pythonhosted.org/packages/80/e6/7aab83774f5d2bca81f42ac58d04caf44f0cc2b65fc6db2b3b2e8a05f3b3/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89", size = 149383, upload-time = "2025-10-14T04:40:46.018Z" },
{ url = "https://files.pythonhosted.org/packages/4f/e8/b289173b4edae05c0dde07f69f8db476a0b511eac556dfe0d6bda3c43384/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569", size = 159098, upload-time = "2025-10-14T04:40:47.081Z" },
{ url = "https://files.pythonhosted.org/packages/d8/df/fe699727754cae3f8478493c7f45f777b17c3ef0600e28abfec8619eb49c/charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224", size = 152991, upload-time = "2025-10-14T04:40:48.246Z" },
{ url = "https://files.pythonhosted.org/packages/1a/86/584869fe4ddb6ffa3bd9f491b87a01568797fb9bd8933f557dba9771beaf/charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a", size = 99456, upload-time = "2025-10-14T04:40:49.376Z" },
{ url = "https://files.pythonhosted.org/packages/65/f6/62fdd5feb60530f50f7e38b4f6a1d5203f4d16ff4f9f0952962c044e919a/charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016", size = 106978, upload-time = "2025-10-14T04:40:50.844Z" },
{ url = "https://files.pythonhosted.org/packages/7a/9d/0710916e6c82948b3be62d9d398cb4fcf4e97b56d6a6aeccd66c4b2f2bd5/charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1", size = 99969, upload-time = "2025-10-14T04:40:52.272Z" },
{ url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425, upload-time = "2025-10-14T04:40:53.353Z" },
{ url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162, upload-time = "2025-10-14T04:40:54.558Z" },
{ url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558, upload-time = "2025-10-14T04:40:55.677Z" },
{ url = "https://files.pythonhosted.org/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", size = 161497, upload-time = "2025-10-14T04:40:57.217Z" },
{ url = "https://files.pythonhosted.org/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", size = 159240, upload-time = "2025-10-14T04:40:58.358Z" },
{ url = "https://files.pythonhosted.org/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", size = 153471, upload-time = "2025-10-14T04:40:59.468Z" },
{ url = "https://files.pythonhosted.org/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", size = 150864, upload-time = "2025-10-14T04:41:00.623Z" },
{ url = "https://files.pythonhosted.org/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", size = 150647, upload-time = "2025-10-14T04:41:01.754Z" },
{ url = "https://files.pythonhosted.org/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", size = 145110, upload-time = "2025-10-14T04:41:03.231Z" },
{ url = "https://files.pythonhosted.org/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", size = 162839, upload-time = "2025-10-14T04:41:04.715Z" },
{ url = "https://files.pythonhosted.org/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", size = 150667, upload-time = "2025-10-14T04:41:05.827Z" },
{ url = "https://files.pythonhosted.org/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", size = 160535, upload-time = "2025-10-14T04:41:06.938Z" },
{ url = "https://files.pythonhosted.org/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", size = 154816, upload-time = "2025-10-14T04:41:08.101Z" },
{ url = "https://files.pythonhosted.org/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694, upload-time = "2025-10-14T04:41:09.23Z" },
{ url = "https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131, upload-time = "2025-10-14T04:41:10.467Z" },
{ url = "https://files.pythonhosted.org/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390, upload-time = "2025-10-14T04:41:11.915Z" },
{ url = "https://files.pythonhosted.org/packages/97/45/4b3a1239bbacd321068ea6e7ac28875b03ab8bc0aa0966452db17cd36714/charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794", size = 208091, upload-time = "2025-10-14T04:41:13.346Z" },
{ url = "https://files.pythonhosted.org/packages/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" },
{ url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" },
@@ -208,6 +266,32 @@ version = "7.11.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/1c/38/ee22495420457259d2f3390309505ea98f98a5eed40901cf62196abad006/coverage-7.11.0.tar.gz", hash = "sha256:167bd504ac1ca2af7ff3b81d245dfea0292c5032ebef9d66cc08a7d28c1b8050", size = 811905, upload-time = "2025-10-15T15:15:08.542Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/49/3a/ee1074c15c408ddddddb1db7dd904f6b81bc524e01f5a1c5920e13dbde23/coverage-7.11.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3d58ecaa865c5b9fa56e35efc51d1014d4c0d22838815b9fce57a27dd9576847", size = 215912, upload-time = "2025-10-15T15:12:40.665Z" },
{ url = "https://files.pythonhosted.org/packages/70/c4/9f44bebe5cb15f31608597b037d78799cc5f450044465bcd1ae8cb222fe1/coverage-7.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b679e171f1c104a5668550ada700e3c4937110dbdd153b7ef9055c4f1a1ee3cc", size = 216310, upload-time = "2025-10-15T15:12:42.461Z" },
{ url = "https://files.pythonhosted.org/packages/42/01/5e06077cfef92d8af926bdd86b84fb28bf9bc6ad27343d68be9b501d89f2/coverage-7.11.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ca61691ba8c5b6797deb221a0d09d7470364733ea9c69425a640f1f01b7c5bf0", size = 246706, upload-time = "2025-10-15T15:12:44.001Z" },
{ url = "https://files.pythonhosted.org/packages/40/b8/7a3f1f33b35cc4a6c37e759137533119560d06c0cc14753d1a803be0cd4a/coverage-7.11.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:aef1747ede4bd8ca9cfc04cc3011516500c6891f1b33a94add3253f6f876b7b7", size = 248634, upload-time = "2025-10-15T15:12:45.768Z" },
{ url = "https://files.pythonhosted.org/packages/7a/41/7f987eb33de386bc4c665ab0bf98d15fcf203369d6aacae74f5dd8ec489a/coverage-7.11.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a1839d08406e4cba2953dcc0ffb312252f14d7c4c96919f70167611f4dee2623", size = 250741, upload-time = "2025-10-15T15:12:47.222Z" },
{ url = "https://files.pythonhosted.org/packages/23/c1/a4e0ca6a4e83069fb8216b49b30a7352061ca0cb38654bd2dc96b7b3b7da/coverage-7.11.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e0eb0a2dcc62478eb5b4cbb80b97bdee852d7e280b90e81f11b407d0b81c4287", size = 246837, upload-time = "2025-10-15T15:12:48.904Z" },
{ url = "https://files.pythonhosted.org/packages/5d/03/ced062a17f7c38b4728ff76c3acb40d8465634b20b4833cdb3cc3a74e115/coverage-7.11.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bc1fbea96343b53f65d5351d8fd3b34fd415a2670d7c300b06d3e14a5af4f552", size = 248429, upload-time = "2025-10-15T15:12:50.73Z" },
{ url = "https://files.pythonhosted.org/packages/97/af/a7c6f194bb8c5a2705ae019036b8fe7f49ea818d638eedb15fdb7bed227c/coverage-7.11.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:214b622259dd0cf435f10241f1333d32caa64dbc27f8790ab693428a141723de", size = 246490, upload-time = "2025-10-15T15:12:52.646Z" },
{ url = "https://files.pythonhosted.org/packages/ab/c3/aab4df02b04a8fde79068c3c41ad7a622b0ef2b12e1ed154da986a727c3f/coverage-7.11.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:258d9967520cca899695d4eb7ea38be03f06951d6ca2f21fb48b1235f791e601", size = 246208, upload-time = "2025-10-15T15:12:54.586Z" },
{ url = "https://files.pythonhosted.org/packages/30/d8/e282ec19cd658238d60ed404f99ef2e45eed52e81b866ab1518c0d4163cf/coverage-7.11.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:cf9e6ff4ca908ca15c157c409d608da77a56a09877b97c889b98fb2c32b6465e", size = 247126, upload-time = "2025-10-15T15:12:56.485Z" },
{ url = "https://files.pythonhosted.org/packages/d1/17/a635fa07fac23adb1a5451ec756216768c2767efaed2e4331710342a3399/coverage-7.11.0-cp311-cp311-win32.whl", hash = "sha256:fcc15fc462707b0680cff6242c48625da7f9a16a28a41bb8fd7a4280920e676c", size = 218314, upload-time = "2025-10-15T15:12:58.365Z" },
{ url = "https://files.pythonhosted.org/packages/2a/29/2ac1dfcdd4ab9a70026edc8d715ece9b4be9a1653075c658ee6f271f394d/coverage-7.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:865965bf955d92790f1facd64fe7ff73551bd2c1e7e6b26443934e9701ba30b9", size = 219203, upload-time = "2025-10-15T15:12:59.902Z" },
{ url = "https://files.pythonhosted.org/packages/03/21/5ce8b3a0133179115af4c041abf2ee652395837cb896614beb8ce8ddcfd9/coverage-7.11.0-cp311-cp311-win_arm64.whl", hash = "sha256:5693e57a065760dcbeb292d60cc4d0231a6d4b6b6f6a3191561e1d5e8820b745", size = 217879, upload-time = "2025-10-15T15:13:01.35Z" },
{ url = "https://files.pythonhosted.org/packages/c4/db/86f6906a7c7edc1a52b2c6682d6dd9be775d73c0dfe2b84f8923dfea5784/coverage-7.11.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9c49e77811cf9d024b95faf86c3f059b11c0c9be0b0d61bc598f453703bd6fd1", size = 216098, upload-time = "2025-10-15T15:13:02.916Z" },
{ url = "https://files.pythonhosted.org/packages/21/54/e7b26157048c7ba555596aad8569ff903d6cd67867d41b75287323678ede/coverage-7.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a61e37a403a778e2cda2a6a39abcc895f1d984071942a41074b5c7ee31642007", size = 216331, upload-time = "2025-10-15T15:13:04.403Z" },
{ url = "https://files.pythonhosted.org/packages/b9/19/1ce6bf444f858b83a733171306134a0544eaddf1ca8851ede6540a55b2ad/coverage-7.11.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c79cae102bb3b1801e2ef1511fb50e91ec83a1ce466b2c7c25010d884336de46", size = 247825, upload-time = "2025-10-15T15:13:05.92Z" },
{ url = "https://files.pythonhosted.org/packages/71/0b/d3bcbbc259fcced5fb67c5d78f6e7ee965f49760c14afd931e9e663a83b2/coverage-7.11.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:16ce17ceb5d211f320b62df002fa7016b7442ea0fd260c11cec8ce7730954893", size = 250573, upload-time = "2025-10-15T15:13:07.471Z" },
{ url = "https://files.pythonhosted.org/packages/58/8d/b0ff3641a320abb047258d36ed1c21d16be33beed4152628331a1baf3365/coverage-7.11.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:80027673e9d0bd6aef86134b0771845e2da85755cf686e7c7c59566cf5a89115", size = 251706, upload-time = "2025-10-15T15:13:09.4Z" },
{ url = "https://files.pythonhosted.org/packages/59/c8/5a586fe8c7b0458053d9c687f5cff515a74b66c85931f7fe17a1c958b4ac/coverage-7.11.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4d3ffa07a08657306cd2215b0da53761c4d73cb54d9143b9303a6481ec0cd415", size = 248221, upload-time = "2025-10-15T15:13:10.964Z" },
{ url = "https://files.pythonhosted.org/packages/d0/ff/3a25e3132804ba44cfa9a778cdf2b73dbbe63ef4b0945e39602fc896ba52/coverage-7.11.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a3b6a5f8b2524fd6c1066bc85bfd97e78709bb5e37b5b94911a6506b65f47186", size = 249624, upload-time = "2025-10-15T15:13:12.5Z" },
{ url = "https://files.pythonhosted.org/packages/c5/12/ff10c8ce3895e1b17a73485ea79ebc1896a9e466a9d0f4aef63e0d17b718/coverage-7.11.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:fcc0a4aa589de34bc56e1a80a740ee0f8c47611bdfb28cd1849de60660f3799d", size = 247744, upload-time = "2025-10-15T15:13:14.554Z" },
{ url = "https://files.pythonhosted.org/packages/16/02/d500b91f5471b2975947e0629b8980e5e90786fe316b6d7299852c1d793d/coverage-7.11.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:dba82204769d78c3fd31b35c3d5f46e06511936c5019c39f98320e05b08f794d", size = 247325, upload-time = "2025-10-15T15:13:16.438Z" },
{ url = "https://files.pythonhosted.org/packages/77/11/dee0284fbbd9cd64cfce806b827452c6df3f100d9e66188e82dfe771d4af/coverage-7.11.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:81b335f03ba67309a95210caf3eb43bd6fe75a4e22ba653ef97b4696c56c7ec2", size = 249180, upload-time = "2025-10-15T15:13:17.959Z" },
{ url = "https://files.pythonhosted.org/packages/59/1b/cdf1def928f0a150a057cab03286774e73e29c2395f0d30ce3d9e9f8e697/coverage-7.11.0-cp312-cp312-win32.whl", hash = "sha256:037b2d064c2f8cc8716fe4d39cb705779af3fbf1ba318dc96a1af858888c7bb5", size = 218479, upload-time = "2025-10-15T15:13:19.608Z" },
{ url = "https://files.pythonhosted.org/packages/ff/55/e5884d55e031da9c15b94b90a23beccc9d6beee65e9835cd6da0a79e4f3a/coverage-7.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:d66c0104aec3b75e5fd897e7940188ea1892ca1d0235316bf89286d6a22568c0", size = 219290, upload-time = "2025-10-15T15:13:21.593Z" },
{ url = "https://files.pythonhosted.org/packages/23/a8/faa930cfc71c1d16bc78f9a19bb73700464f9c331d9e547bfbc1dbd3a108/coverage-7.11.0-cp312-cp312-win_arm64.whl", hash = "sha256:d91ebeac603812a09cf6a886ba6e464f3bbb367411904ae3790dfe28311b15ad", size = 217924, upload-time = "2025-10-15T15:13:23.39Z" },
{ url = "https://files.pythonhosted.org/packages/60/7f/85e4dfe65e400645464b25c036a26ac226cf3a69d4a50c3934c532491cdd/coverage-7.11.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:cc3f49e65ea6e0d5d9bd60368684fe52a704d46f9e7fc413918f18d046ec40e1", size = 216129, upload-time = "2025-10-15T15:13:25.371Z" },
{ url = "https://files.pythonhosted.org/packages/96/5d/dc5fa98fea3c175caf9d360649cb1aa3715e391ab00dc78c4c66fabd7356/coverage-7.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f39ae2f63f37472c17b4990f794035c9890418b1b8cca75c01193f3c8d3e01be", size = 216380, upload-time = "2025-10-15T15:13:26.976Z" },
{ url = "https://files.pythonhosted.org/packages/b2/f5/3da9cc9596708273385189289c0e4d8197d37a386bdf17619013554b3447/coverage-7.11.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7db53b5cdd2917b6eaadd0b1251cf4e7d96f4a8d24e174bdbdf2f65b5ea7994d", size = 247375, upload-time = "2025-10-15T15:13:28.923Z" },
@@ -263,6 +347,11 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/5f/04/642c1d8a448ae5ea1369eac8495740a79eb4e581a9fb0cbdce56bbf56da1/coverage-7.11.0-py3-none-any.whl", hash = "sha256:4b7589765348d78fb4e5fb6ea35d07564e387da2fc5efff62e0222971f155f68", size = 207761, upload-time = "2025-10-15T15:15:06.439Z" },
]
[package.optional-dependencies]
toml = [
{ name = "tomli", marker = "python_full_version <= '3.11'" },
]
[[package]]
name = "cryptography"
version = "46.0.3"
@@ -317,6 +406,12 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/0a/6e/1c8331ddf91ca4730ab3086a0f1be19c65510a33b5a441cb334e7a2d2560/cryptography-46.0.3-cp38-abi3-win32.whl", hash = "sha256:6276eb85ef938dc035d59b87c8a7dc559a232f954962520137529d77b18ff1df", size = 3036695, upload-time = "2025-10-15T23:18:08.672Z" },
{ url = "https://files.pythonhosted.org/packages/90/45/b0d691df20633eff80955a0fc7695ff9051ffce8b69741444bd9ed7bd0db/cryptography-46.0.3-cp38-abi3-win_amd64.whl", hash = "sha256:416260257577718c05135c55958b674000baef9a1c7d9e8f306ec60d71db850f", size = 3501720, upload-time = "2025-10-15T23:18:10.632Z" },
{ url = "https://files.pythonhosted.org/packages/e8/cb/2da4cc83f5edb9c3257d09e1e7ab7b23f049c7962cae8d842bbef0a9cec9/cryptography-46.0.3-cp38-abi3-win_arm64.whl", hash = "sha256:d89c3468de4cdc4f08a57e214384d0471911a3830fcdaf7a8cc587e42a866372", size = 2918740, upload-time = "2025-10-15T23:18:12.277Z" },
{ url = "https://files.pythonhosted.org/packages/06/8a/e60e46adab4362a682cf142c7dcb5bf79b782ab2199b0dcb81f55970807f/cryptography-46.0.3-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:7ce938a99998ed3c8aa7e7272dca1a610401ede816d36d0693907d863b10d9ea", size = 3698132, upload-time = "2025-10-15T23:18:17.056Z" },
{ url = "https://files.pythonhosted.org/packages/da/38/f59940ec4ee91e93d3311f7532671a5cef5570eb04a144bf203b58552d11/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:191bb60a7be5e6f54e30ba16fdfae78ad3a342a0599eb4193ba88e3f3d6e185b", size = 4243992, upload-time = "2025-10-15T23:18:18.695Z" },
{ url = "https://files.pythonhosted.org/packages/b0/0c/35b3d92ddebfdfda76bb485738306545817253d0a3ded0bfe80ef8e67aa5/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c70cc23f12726be8f8bc72e41d5065d77e4515efae3690326764ea1b07845cfb", size = 4409944, upload-time = "2025-10-15T23:18:20.597Z" },
{ url = "https://files.pythonhosted.org/packages/99/55/181022996c4063fc0e7666a47049a1ca705abb9c8a13830f074edb347495/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:9394673a9f4de09e28b5356e7fff97d778f8abad85c9d5ac4a4b7e25a0de7717", size = 4242957, upload-time = "2025-10-15T23:18:22.18Z" },
{ url = "https://files.pythonhosted.org/packages/ba/af/72cd6ef29f9c5f731251acadaeb821559fe25f10852f44a63374c9ca08c1/cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:94cd0549accc38d1494e1f8de71eca837d0509d0d44bf11d158524b0e12cebf9", size = 4409447, upload-time = "2025-10-15T23:18:24.209Z" },
{ url = "https://files.pythonhosted.org/packages/0d/c3/e90f4a4feae6410f914f8ebac129b9ae7a8c92eb60a638012dde42030a9d/cryptography-46.0.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:6b5063083824e5509fdba180721d55909ffacccc8adbec85268b48439423d78c", size = 3438528, upload-time = "2025-10-15T23:18:26.227Z" },
]
[[package]]
@@ -441,6 +536,20 @@ version = "0.7.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/b5/46/120a669232c7bdedb9d52d4aeae7e6c7dfe151e99dc70802e2fc7a5e1993/httptools-0.7.1.tar.gz", hash = "sha256:abd72556974f8e7c74a259655924a717a2365b236c882c3f6f8a45fe94703ac9", size = 258961, upload-time = "2025-10-10T03:55:08.559Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/9c/08/17e07e8d89ab8f343c134616d72eebfe03798835058e2ab579dcc8353c06/httptools-0.7.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:474d3b7ab469fefcca3697a10d11a32ee2b9573250206ba1e50d5980910da657", size = 206521, upload-time = "2025-10-10T03:54:31.002Z" },
{ url = "https://files.pythonhosted.org/packages/aa/06/c9c1b41ff52f16aee526fd10fbda99fa4787938aa776858ddc4a1ea825ec/httptools-0.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a3c3b7366bb6c7b96bd72d0dbe7f7d5eead261361f013be5f6d9590465ea1c70", size = 110375, upload-time = "2025-10-10T03:54:31.941Z" },
{ url = "https://files.pythonhosted.org/packages/cc/cc/10935db22fda0ee34c76f047590ca0a8bd9de531406a3ccb10a90e12ea21/httptools-0.7.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:379b479408b8747f47f3b253326183d7c009a3936518cdb70db58cffd369d9df", size = 456621, upload-time = "2025-10-10T03:54:33.176Z" },
{ url = "https://files.pythonhosted.org/packages/0e/84/875382b10d271b0c11aa5d414b44f92f8dd53e9b658aec338a79164fa548/httptools-0.7.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cad6b591a682dcc6cf1397c3900527f9affef1e55a06c4547264796bbd17cf5e", size = 454954, upload-time = "2025-10-10T03:54:34.226Z" },
{ url = "https://files.pythonhosted.org/packages/30/e1/44f89b280f7e46c0b1b2ccee5737d46b3bb13136383958f20b580a821ca0/httptools-0.7.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:eb844698d11433d2139bbeeb56499102143beb582bd6c194e3ba69c22f25c274", size = 440175, upload-time = "2025-10-10T03:54:35.942Z" },
{ url = "https://files.pythonhosted.org/packages/6f/7e/b9287763159e700e335028bc1824359dc736fa9b829dacedace91a39b37e/httptools-0.7.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f65744d7a8bdb4bda5e1fa23e4ba16832860606fcc09d674d56e425e991539ec", size = 440310, upload-time = "2025-10-10T03:54:37.1Z" },
{ url = "https://files.pythonhosted.org/packages/b3/07/5b614f592868e07f5c94b1f301b5e14a21df4e8076215a3bccb830a687d8/httptools-0.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:135fbe974b3718eada677229312e97f3b31f8a9c8ffa3ae6f565bf808d5b6bcb", size = 86875, upload-time = "2025-10-10T03:54:38.421Z" },
{ url = "https://files.pythonhosted.org/packages/53/7f/403e5d787dc4942316e515e949b0c8a013d84078a915910e9f391ba9b3ed/httptools-0.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:38e0c83a2ea9746ebbd643bdfb521b9aa4a91703e2cd705c20443405d2fd16a5", size = 206280, upload-time = "2025-10-10T03:54:39.274Z" },
{ url = "https://files.pythonhosted.org/packages/2a/0d/7f3fd28e2ce311ccc998c388dd1c53b18120fda3b70ebb022b135dc9839b/httptools-0.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f25bbaf1235e27704f1a7b86cd3304eabc04f569c828101d94a0e605ef7205a5", size = 110004, upload-time = "2025-10-10T03:54:40.403Z" },
{ url = "https://files.pythonhosted.org/packages/84/a6/b3965e1e146ef5762870bbe76117876ceba51a201e18cc31f5703e454596/httptools-0.7.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2c15f37ef679ab9ecc06bfc4e6e8628c32a8e4b305459de7cf6785acd57e4d03", size = 517655, upload-time = "2025-10-10T03:54:41.347Z" },
{ url = "https://files.pythonhosted.org/packages/11/7d/71fee6f1844e6fa378f2eddde6c3e41ce3a1fb4b2d81118dd544e3441ec0/httptools-0.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7fe6e96090df46b36ccfaf746f03034e5ab723162bc51b0a4cf58305324036f2", size = 511440, upload-time = "2025-10-10T03:54:42.452Z" },
{ url = "https://files.pythonhosted.org/packages/22/a5/079d216712a4f3ffa24af4a0381b108aa9c45b7a5cc6eb141f81726b1823/httptools-0.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f72fdbae2dbc6e68b8239defb48e6a5937b12218e6ffc2c7846cc37befa84362", size = 495186, upload-time = "2025-10-10T03:54:43.937Z" },
{ url = "https://files.pythonhosted.org/packages/e9/9e/025ad7b65278745dee3bd0ebf9314934c4592560878308a6121f7f812084/httptools-0.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e99c7b90a29fd82fea9ef57943d501a16f3404d7b9ee81799d41639bdaae412c", size = 499192, upload-time = "2025-10-10T03:54:45.003Z" },
{ url = "https://files.pythonhosted.org/packages/6d/de/40a8f202b987d43afc4d54689600ff03ce65680ede2f31df348d7f368b8f/httptools-0.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:3e14f530fefa7499334a79b0cf7e7cd2992870eb893526fb097d51b4f2d0f321", size = 86694, upload-time = "2025-10-10T03:54:45.923Z" },
{ url = "https://files.pythonhosted.org/packages/09/8f/c77b1fcbfd262d422f12da02feb0d218fa228d52485b77b953832105bb90/httptools-0.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6babce6cfa2a99545c60bfef8bee0cc0545413cb0018f617c8059a30ad985de3", size = 202889, upload-time = "2025-10-10T03:54:47.089Z" },
{ url = "https://files.pythonhosted.org/packages/0a/1a/22887f53602feaa066354867bc49a68fc295c2293433177ee90870a7d517/httptools-0.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:601b7628de7504077dd3dcb3791c6b8694bbd967148a6d1f01806509254fb1ca", size = 108180, upload-time = "2025-10-10T03:54:48.052Z" },
{ url = "https://files.pythonhosted.org/packages/32/6a/6aaa91937f0010d288d3d124ca2946d48d60c3a5ee7ca62afe870e3ea011/httptools-0.7.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:04c6c0e6c5fb0739c5b8a9eb046d298650a0ff38cf42537fc372b28dc7e4472c", size = 478596, upload-time = "2025-10-10T03:54:48.919Z" },
@@ -520,6 +629,28 @@ version = "3.0.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/08/db/fefacb2136439fc8dd20e797950e749aa1f4997ed584c62cfb8ef7c2be0e/markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad", size = 11631, upload-time = "2025-09-27T18:36:18.185Z" },
{ url = "https://files.pythonhosted.org/packages/e1/2e/5898933336b61975ce9dc04decbc0a7f2fee78c30353c5efba7f2d6ff27a/markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a", size = 12058, upload-time = "2025-09-27T18:36:19.444Z" },
{ url = "https://files.pythonhosted.org/packages/1d/09/adf2df3699d87d1d8184038df46a9c80d78c0148492323f4693df54e17bb/markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50", size = 24287, upload-time = "2025-09-27T18:36:20.768Z" },
{ url = "https://files.pythonhosted.org/packages/30/ac/0273f6fcb5f42e314c6d8cd99effae6a5354604d461b8d392b5ec9530a54/markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf", size = 22940, upload-time = "2025-09-27T18:36:22.249Z" },
{ url = "https://files.pythonhosted.org/packages/19/ae/31c1be199ef767124c042c6c3e904da327a2f7f0cd63a0337e1eca2967a8/markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f", size = 21887, upload-time = "2025-09-27T18:36:23.535Z" },
{ url = "https://files.pythonhosted.org/packages/b2/76/7edcab99d5349a4532a459e1fe64f0b0467a3365056ae550d3bcf3f79e1e/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a", size = 23692, upload-time = "2025-09-27T18:36:24.823Z" },
{ url = "https://files.pythonhosted.org/packages/a4/28/6e74cdd26d7514849143d69f0bf2399f929c37dc2b31e6829fd2045b2765/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115", size = 21471, upload-time = "2025-09-27T18:36:25.95Z" },
{ url = "https://files.pythonhosted.org/packages/62/7e/a145f36a5c2945673e590850a6f8014318d5577ed7e5920a4b3448e0865d/markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a", size = 22923, upload-time = "2025-09-27T18:36:27.109Z" },
{ url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" },
{ url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" },
{ url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" },
{ url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" },
{ url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" },
{ url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" },
{ url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" },
{ url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" },
{ url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" },
{ url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" },
{ url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" },
{ url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" },
{ url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" },
{ url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" },
{ url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" },
{ url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" },
{ url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" },
@@ -635,6 +766,34 @@ dependencies = [
]
sdist = { url = "https://files.pythonhosted.org/packages/df/18/d0944e8eaaa3efd0a91b0f1fc537d3be55ad35091b6a87638211ba691964/pydantic_core-2.41.4.tar.gz", hash = "sha256:70e47929a9d4a1905a67e4b687d5946026390568a8e952b92824118063cee4d5", size = 457557, upload-time = "2025-10-14T10:23:47.909Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/62/4c/f6cbfa1e8efacd00b846764e8484fe173d25b8dab881e277a619177f3384/pydantic_core-2.41.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:28ff11666443a1a8cf2a044d6a545ebffa8382b5f7973f22c36109205e65dc80", size = 2109062, upload-time = "2025-10-14T10:20:04.486Z" },
{ url = "https://files.pythonhosted.org/packages/21/f8/40b72d3868896bfcd410e1bd7e516e762d326201c48e5b4a06446f6cf9e8/pydantic_core-2.41.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:61760c3925d4633290292bad462e0f737b840508b4f722247d8729684f6539ae", size = 1916301, upload-time = "2025-10-14T10:20:06.857Z" },
{ url = "https://files.pythonhosted.org/packages/94/4d/d203dce8bee7faeca791671c88519969d98d3b4e8f225da5b96dad226fc8/pydantic_core-2.41.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eae547b7315d055b0de2ec3965643b0ab82ad0106a7ffd29615ee9f266a02827", size = 1968728, upload-time = "2025-10-14T10:20:08.353Z" },
{ url = "https://files.pythonhosted.org/packages/65/f5/6a66187775df87c24d526985b3a5d78d861580ca466fbd9d4d0e792fcf6c/pydantic_core-2.41.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ef9ee5471edd58d1fcce1c80ffc8783a650e3e3a193fe90d52e43bb4d87bff1f", size = 2050238, upload-time = "2025-10-14T10:20:09.766Z" },
{ url = "https://files.pythonhosted.org/packages/5e/b9/78336345de97298cf53236b2f271912ce11f32c1e59de25a374ce12f9cce/pydantic_core-2.41.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:15dd504af121caaf2c95cb90c0ebf71603c53de98305621b94da0f967e572def", size = 2249424, upload-time = "2025-10-14T10:20:11.732Z" },
{ url = "https://files.pythonhosted.org/packages/99/bb/a4584888b70ee594c3d374a71af5075a68654d6c780369df269118af7402/pydantic_core-2.41.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3a926768ea49a8af4d36abd6a8968b8790f7f76dd7cbd5a4c180db2b4ac9a3a2", size = 2366047, upload-time = "2025-10-14T10:20:13.647Z" },
{ url = "https://files.pythonhosted.org/packages/5f/8d/17fc5de9d6418e4d2ae8c675f905cdafdc59d3bf3bf9c946b7ab796a992a/pydantic_core-2.41.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6916b9b7d134bff5440098a4deb80e4cb623e68974a87883299de9124126c2a8", size = 2071163, upload-time = "2025-10-14T10:20:15.307Z" },
{ url = "https://files.pythonhosted.org/packages/54/e7/03d2c5c0b8ed37a4617430db68ec5e7dbba66358b629cd69e11b4d564367/pydantic_core-2.41.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5cf90535979089df02e6f17ffd076f07237efa55b7343d98760bde8743c4b265", size = 2190585, upload-time = "2025-10-14T10:20:17.3Z" },
{ url = "https://files.pythonhosted.org/packages/be/fc/15d1c9fe5ad9266a5897d9b932b7f53d7e5cfc800573917a2c5d6eea56ec/pydantic_core-2.41.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:7533c76fa647fade2d7ec75ac5cc079ab3f34879626dae5689b27790a6cf5a5c", size = 2150109, upload-time = "2025-10-14T10:20:19.143Z" },
{ url = "https://files.pythonhosted.org/packages/26/ef/e735dd008808226c83ba56972566138665b71477ad580fa5a21f0851df48/pydantic_core-2.41.4-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:37e516bca9264cbf29612539801ca3cd5d1be465f940417b002905e6ed79d38a", size = 2315078, upload-time = "2025-10-14T10:20:20.742Z" },
{ url = "https://files.pythonhosted.org/packages/90/00/806efdcf35ff2ac0f938362350cd9827b8afb116cc814b6b75cf23738c7c/pydantic_core-2.41.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:0c19cb355224037c83642429b8ce261ae108e1c5fbf5c028bac63c77b0f8646e", size = 2318737, upload-time = "2025-10-14T10:20:22.306Z" },
{ url = "https://files.pythonhosted.org/packages/41/7e/6ac90673fe6cb36621a2283552897838c020db343fa86e513d3f563b196f/pydantic_core-2.41.4-cp311-cp311-win32.whl", hash = "sha256:09c2a60e55b357284b5f31f5ab275ba9f7f70b7525e18a132ec1f9160b4f1f03", size = 1974160, upload-time = "2025-10-14T10:20:23.817Z" },
{ url = "https://files.pythonhosted.org/packages/e0/9d/7c5e24ee585c1f8b6356e1d11d40ab807ffde44d2db3b7dfd6d20b09720e/pydantic_core-2.41.4-cp311-cp311-win_amd64.whl", hash = "sha256:711156b6afb5cb1cb7c14a2cc2c4a8b4c717b69046f13c6b332d8a0a8f41ca3e", size = 2021883, upload-time = "2025-10-14T10:20:25.48Z" },
{ url = "https://files.pythonhosted.org/packages/33/90/5c172357460fc28b2871eb4a0fb3843b136b429c6fa827e4b588877bf115/pydantic_core-2.41.4-cp311-cp311-win_arm64.whl", hash = "sha256:6cb9cf7e761f4f8a8589a45e49ed3c0d92d1d696a45a6feaee8c904b26efc2db", size = 1968026, upload-time = "2025-10-14T10:20:27.039Z" },
{ url = "https://files.pythonhosted.org/packages/e9/81/d3b3e95929c4369d30b2a66a91db63c8ed0a98381ae55a45da2cd1cc1288/pydantic_core-2.41.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:ab06d77e053d660a6faaf04894446df7b0a7e7aba70c2797465a0a1af00fc887", size = 2099043, upload-time = "2025-10-14T10:20:28.561Z" },
{ url = "https://files.pythonhosted.org/packages/58/da/46fdac49e6717e3a94fc9201403e08d9d61aa7a770fab6190b8740749047/pydantic_core-2.41.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c53ff33e603a9c1179a9364b0a24694f183717b2e0da2b5ad43c316c956901b2", size = 1910699, upload-time = "2025-10-14T10:20:30.217Z" },
{ url = "https://files.pythonhosted.org/packages/1e/63/4d948f1b9dd8e991a5a98b77dd66c74641f5f2e5225fee37994b2e07d391/pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:304c54176af2c143bd181d82e77c15c41cbacea8872a2225dd37e6544dce9999", size = 1952121, upload-time = "2025-10-14T10:20:32.246Z" },
{ url = "https://files.pythonhosted.org/packages/b2/a7/e5fc60a6f781fc634ecaa9ecc3c20171d238794cef69ae0af79ac11b89d7/pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:025ba34a4cf4fb32f917d5d188ab5e702223d3ba603be4d8aca2f82bede432a4", size = 2041590, upload-time = "2025-10-14T10:20:34.332Z" },
{ url = "https://files.pythonhosted.org/packages/70/69/dce747b1d21d59e85af433428978a1893c6f8a7068fa2bb4a927fba7a5ff/pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b9f5f30c402ed58f90c70e12eff65547d3ab74685ffe8283c719e6bead8ef53f", size = 2219869, upload-time = "2025-10-14T10:20:35.965Z" },
{ url = "https://files.pythonhosted.org/packages/83/6a/c070e30e295403bf29c4df1cb781317b6a9bac7cd07b8d3acc94d501a63c/pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd96e5d15385d301733113bcaa324c8bcf111275b7675a9c6e88bfb19fc05e3b", size = 2345169, upload-time = "2025-10-14T10:20:37.627Z" },
{ url = "https://files.pythonhosted.org/packages/f0/83/06d001f8043c336baea7fd202a9ac7ad71f87e1c55d8112c50b745c40324/pydantic_core-2.41.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98f348cbb44fae6e9653c1055db7e29de67ea6a9ca03a5fa2c2e11a47cff0e47", size = 2070165, upload-time = "2025-10-14T10:20:39.246Z" },
{ url = "https://files.pythonhosted.org/packages/14/0a/e567c2883588dd12bcbc110232d892cf385356f7c8a9910311ac997ab715/pydantic_core-2.41.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ec22626a2d14620a83ca583c6f5a4080fa3155282718b6055c2ea48d3ef35970", size = 2189067, upload-time = "2025-10-14T10:20:41.015Z" },
{ url = "https://files.pythonhosted.org/packages/f4/1d/3d9fca34273ba03c9b1c5289f7618bc4bd09c3ad2289b5420481aa051a99/pydantic_core-2.41.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3a95d4590b1f1a43bf33ca6d647b990a88f4a3824a8c4572c708f0b45a5290ed", size = 2132997, upload-time = "2025-10-14T10:20:43.106Z" },
{ url = "https://files.pythonhosted.org/packages/52/70/d702ef7a6cd41a8afc61f3554922b3ed8d19dd54c3bd4bdbfe332e610827/pydantic_core-2.41.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:f9672ab4d398e1b602feadcffcdd3af44d5f5e6ddc15bc7d15d376d47e8e19f8", size = 2307187, upload-time = "2025-10-14T10:20:44.849Z" },
{ url = "https://files.pythonhosted.org/packages/68/4c/c06be6e27545d08b802127914156f38d10ca287a9e8489342793de8aae3c/pydantic_core-2.41.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:84d8854db5f55fead3b579f04bda9a36461dab0730c5d570e1526483e7bb8431", size = 2305204, upload-time = "2025-10-14T10:20:46.781Z" },
{ url = "https://files.pythonhosted.org/packages/b0/e5/35ae4919bcd9f18603419e23c5eaf32750224a89d41a8df1a3704b69f77e/pydantic_core-2.41.4-cp312-cp312-win32.whl", hash = "sha256:9be1c01adb2ecc4e464392c36d17f97e9110fbbc906bcbe1c943b5b87a74aabd", size = 1972536, upload-time = "2025-10-14T10:20:48.39Z" },
{ url = "https://files.pythonhosted.org/packages/1e/c2/49c5bb6d2a49eb2ee3647a93e3dae7080c6409a8a7558b075027644e879c/pydantic_core-2.41.4-cp312-cp312-win_amd64.whl", hash = "sha256:d682cf1d22bab22a5be08539dca3d1593488a99998f9f412137bc323179067ff", size = 2031132, upload-time = "2025-10-14T10:20:50.421Z" },
{ url = "https://files.pythonhosted.org/packages/06/23/936343dbcba6eec93f73e95eb346810fc732f71ba27967b287b66f7b7097/pydantic_core-2.41.4-cp312-cp312-win_arm64.whl", hash = "sha256:833eebfd75a26d17470b58768c1834dfc90141b7afc6eb0429c21fc5a21dcfb8", size = 1969483, upload-time = "2025-10-14T10:20:52.35Z" },
{ url = "https://files.pythonhosted.org/packages/13/d0/c20adabd181a029a970738dfe23710b52a31f1258f591874fcdec7359845/pydantic_core-2.41.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:85e050ad9e5f6fe1004eec65c914332e52f429bc0ae12d6fa2092407a462c746", size = 2105688, upload-time = "2025-10-14T10:20:54.448Z" },
{ url = "https://files.pythonhosted.org/packages/00/b6/0ce5c03cec5ae94cca220dfecddc453c077d71363b98a4bbdb3c0b22c783/pydantic_core-2.41.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e7393f1d64792763a48924ba31d1e44c2cfbc05e3b1c2c9abb4ceeadd912cced", size = 1910807, upload-time = "2025-10-14T10:20:56.115Z" },
{ url = "https://files.pythonhosted.org/packages/68/3e/800d3d02c8beb0b5c069c870cbb83799d085debf43499c897bb4b4aaff0d/pydantic_core-2.41.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:94dab0940b0d1fb28bcab847adf887c66a27a40291eedf0b473be58761c9799a", size = 1956669, upload-time = "2025-10-14T10:20:57.874Z" },
@@ -673,6 +832,22 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/1e/29/b53a9ca6cd366bfc928823679c6a76c7a4c69f8201c0ba7903ad18ebae2f/pydantic_core-2.41.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5729225de81fb65b70fdb1907fcf08c75d498f4a6f15af005aabb1fdadc19dfa", size = 2041183, upload-time = "2025-10-14T10:22:08.812Z" },
{ url = "https://files.pythonhosted.org/packages/c7/3d/f8c1a371ceebcaf94d6dd2d77c6cf4b1c078e13a5837aee83f760b4f7cfd/pydantic_core-2.41.4-cp314-cp314t-win_amd64.whl", hash = "sha256:de2cfbb09e88f0f795fd90cf955858fc2c691df65b1f21f0aa00b99f3fbc661d", size = 1993542, upload-time = "2025-10-14T10:22:11.332Z" },
{ url = "https://files.pythonhosted.org/packages/8a/ac/9fc61b4f9d079482a290afe8d206b8f490e9fd32d4fc03ed4fc698214e01/pydantic_core-2.41.4-cp314-cp314t-win_arm64.whl", hash = "sha256:d34f950ae05a83e0ede899c595f312ca976023ea1db100cd5aa188f7005e3ab0", size = 1973897, upload-time = "2025-10-14T10:22:13.444Z" },
{ url = "https://files.pythonhosted.org/packages/b0/12/5ba58daa7f453454464f92b3ca7b9d7c657d8641c48e370c3ebc9a82dd78/pydantic_core-2.41.4-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:a1b2cfec3879afb742a7b0bcfa53e4f22ba96571c9e54d6a3afe1052d17d843b", size = 2122139, upload-time = "2025-10-14T10:22:47.288Z" },
{ url = "https://files.pythonhosted.org/packages/21/fb/6860126a77725c3108baecd10fd3d75fec25191d6381b6eb2ac660228eac/pydantic_core-2.41.4-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:d175600d975b7c244af6eb9c9041f10059f20b8bbffec9e33fdd5ee3f67cdc42", size = 1936674, upload-time = "2025-10-14T10:22:49.555Z" },
{ url = "https://files.pythonhosted.org/packages/de/be/57dcaa3ed595d81f8757e2b44a38240ac5d37628bce25fb20d02c7018776/pydantic_core-2.41.4-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0f184d657fa4947ae5ec9c47bd7e917730fa1cbb78195037e32dcbab50aca5ee", size = 1956398, upload-time = "2025-10-14T10:22:52.19Z" },
{ url = "https://files.pythonhosted.org/packages/2f/1d/679a344fadb9695f1a6a294d739fbd21d71fa023286daeea8c0ed49e7c2b/pydantic_core-2.41.4-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ed810568aeffed3edc78910af32af911c835cc39ebbfacd1f0ab5dd53028e5c", size = 2138674, upload-time = "2025-10-14T10:22:54.499Z" },
{ url = "https://files.pythonhosted.org/packages/c4/48/ae937e5a831b7c0dc646b2ef788c27cd003894882415300ed21927c21efa/pydantic_core-2.41.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:4f5d640aeebb438517150fdeec097739614421900e4a08db4a3ef38898798537", size = 2112087, upload-time = "2025-10-14T10:22:56.818Z" },
{ url = "https://files.pythonhosted.org/packages/5e/db/6db8073e3d32dae017da7e0d16a9ecb897d0a4d92e00634916e486097961/pydantic_core-2.41.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:4a9ab037b71927babc6d9e7fc01aea9e66dc2a4a34dff06ef0724a4049629f94", size = 1920387, upload-time = "2025-10-14T10:22:59.342Z" },
{ url = "https://files.pythonhosted.org/packages/0d/c1/dd3542d072fcc336030d66834872f0328727e3b8de289c662faa04aa270e/pydantic_core-2.41.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4dab9484ec605c3016df9ad4fd4f9a390bc5d816a3b10c6550f8424bb80b18c", size = 1951495, upload-time = "2025-10-14T10:23:02.089Z" },
{ url = "https://files.pythonhosted.org/packages/2b/c6/db8d13a1f8ab3f1eb08c88bd00fd62d44311e3456d1e85c0e59e0a0376e7/pydantic_core-2.41.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8a5028425820731d8c6c098ab642d7b8b999758e24acae03ed38a66eca8335", size = 2139008, upload-time = "2025-10-14T10:23:04.539Z" },
{ url = "https://files.pythonhosted.org/packages/7e/7d/138e902ed6399b866f7cfe4435d22445e16fff888a1c00560d9dc79a780f/pydantic_core-2.41.4-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:491535d45cd7ad7e4a2af4a5169b0d07bebf1adfd164b0368da8aa41e19907a5", size = 2104721, upload-time = "2025-10-14T10:23:26.906Z" },
{ url = "https://files.pythonhosted.org/packages/47/13/0525623cf94627f7b53b4c2034c81edc8491cbfc7c28d5447fa318791479/pydantic_core-2.41.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:54d86c0cada6aba4ec4c047d0e348cbad7063b87ae0f005d9f8c9ad04d4a92a2", size = 1931608, upload-time = "2025-10-14T10:23:29.306Z" },
{ url = "https://files.pythonhosted.org/packages/d6/f9/744bc98137d6ef0a233f808bfc9b18cf94624bf30836a18d3b05d08bf418/pydantic_core-2.41.4-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eca1124aced216b2500dc2609eade086d718e8249cb9696660ab447d50a758bd", size = 2132986, upload-time = "2025-10-14T10:23:32.057Z" },
{ url = "https://files.pythonhosted.org/packages/17/c8/629e88920171173f6049386cc71f893dff03209a9ef32b4d2f7e7c264bcf/pydantic_core-2.41.4-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6c9024169becccf0cb470ada03ee578d7348c119a0d42af3dcf9eda96e3a247c", size = 2187516, upload-time = "2025-10-14T10:23:34.871Z" },
{ url = "https://files.pythonhosted.org/packages/2e/0f/4f2734688d98488782218ca61bcc118329bf5de05bb7fe3adc7dd79b0b86/pydantic_core-2.41.4-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:26895a4268ae5a2849269f4991cdc97236e4b9c010e51137becf25182daac405", size = 2146146, upload-time = "2025-10-14T10:23:37.342Z" },
{ url = "https://files.pythonhosted.org/packages/ed/f2/ab385dbd94a052c62224b99cf99002eee99dbec40e10006c78575aead256/pydantic_core-2.41.4-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:ca4df25762cf71308c446e33c9b1fdca2923a3f13de616e2a949f38bf21ff5a8", size = 2311296, upload-time = "2025-10-14T10:23:40.145Z" },
{ url = "https://files.pythonhosted.org/packages/fc/8e/e4f12afe1beeb9823bba5375f8f258df0cc61b056b0195fb1cf9f62a1a58/pydantic_core-2.41.4-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:5a28fcedd762349519276c36634e71853b4541079cab4acaaac60c4421827308", size = 2315386, upload-time = "2025-10-14T10:23:42.624Z" },
{ url = "https://files.pythonhosted.org/packages/48/f7/925f65d930802e3ea2eb4d5afa4cb8730c8dc0d2cb89a59dc4ed2fcb2d74/pydantic_core-2.41.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c173ddcd86afd2535e2b695217e82191580663a1d1928239f877f5a1649ef39f", size = 2147775, upload-time = "2025-10-14T10:23:45.406Z" },
]
[[package]]
@@ -724,6 +899,7 @@ version = "1.2.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pytest" },
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/42/86/9e3c5f48f7b7b638b216e4b9e645f54d199d7abbbab7a64a13b4e12ba10f/pytest_asyncio-1.2.0.tar.gz", hash = "sha256:c609a64a2a8768462d0c99811ddb8bd2583c33fd33cf7f21af1c142e824ffb57", size = 50119, upload-time = "2025-09-12T07:33:53.816Z" }
wheels = [
@@ -735,7 +911,7 @@ name = "pytest-cov"
version = "7.0.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "coverage" },
{ name = "coverage", extra = ["toml"] },
{ name = "pluggy" },
{ name = "pytest" },
]
@@ -795,6 +971,25 @@ version = "6.0.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" },
{ url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" },
{ url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" },
{ url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" },
{ url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" },
{ url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" },
{ url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" },
{ url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" },
{ url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" },
{ url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" },
{ url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" },
{ url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" },
{ url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" },
{ url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" },
{ url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" },
{ url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" },
{ url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" },
{ url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" },
{ url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" },
{ url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" },
{ url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" },
{ url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" },
@@ -860,7 +1055,7 @@ dev = [
[package.metadata]
requires-dist = [
{ name = "flet", extras = ["all"], specifier = ">=0.28.3,<0.29.0" },
{ name = "rns", specifier = ">=1.0.0,<1.5.0" },
{ name = "rns", specifier = ">=1.0.2,<1.5.0" },
]
[package.metadata.requires-dev]
@@ -914,15 +1109,15 @@ wheels = [
[[package]]
name = "rns"
version = "1.0.1"
version = "1.0.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cryptography" },
{ name = "pyserial" },
]
sdist = { url = "https://files.pythonhosted.org/packages/6c/41/be3c0e23b0df82fe7298e4c88c690f52d737efafeaeba3f739bd81f90ee0/rns-1.0.1.tar.gz", hash = "sha256:f45ea52b065be09b8e2257425b6fcde1a49899ea41aee349936d182aa1844b26", size = 364380, upload-time = "2025-11-02T21:57:46.234Z" }
sdist = { url = "https://files.pythonhosted.org/packages/84/a6/7da3bc34c5fd61114484f24561d9647605e1f48bd4a8ca217df58b30508f/rns-1.0.2.tar.gz", hash = "sha256:19c025dadc4a85fc37c751e0e892f446456800ca8c434e007c25d8fd6939687e", size = 364680, upload-time = "2025-11-10T18:04:46.9Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/57/e7/ff7596ebb9614c9ee0f6eeaa4ee6d86ce26ce1ca2f9a60b7ecff79a75155/rns-1.0.1-py3-none-any.whl", hash = "sha256:aa77b4c8e1b6899117666e1e55b05b3250416ab5fea2826254358ae320e8b3ed", size = 428681, upload-time = "2025-11-02T21:57:43.42Z" },
{ url = "https://files.pythonhosted.org/packages/74/6c/1b78dcecee1cf564d17557282bab5e88cfab1b8002d82be79930ed9080fb/rns-1.0.2-py3-none-any.whl", hash = "sha256:723bcf0a839025060ff680c4202b09fa766b35093a4a08506bb85485b8a1f154", size = 428989, upload-time = "2025-11-10T18:04:44.242Z" },
]
[[package]]
@@ -975,6 +1170,7 @@ version = "0.49.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/de/1a/608df0b10b53b0beb96a37854ee05864d182ddd4b1156a22f1ad3860425a/starlette-0.49.3.tar.gz", hash = "sha256:1c14546f299b5901a1ea0e34410575bc33bbd741377a10484a54445588d00284", size = 2655031, upload-time = "2025-11-01T15:12:26.13Z" }
wheels = [
@@ -999,6 +1195,55 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/44/6f/7120676b6d73228c96e17f1f794d8ab046fc910d781c8d151120c3f1569e/toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b", size = 16588, upload-time = "2020-11-01T01:40:20.672Z" },
]
[[package]]
name = "tomli"
version = "2.3.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/52/ed/3f73f72945444548f33eba9a87fc7a6e969915e7b1acc8260b30e1f76a2f/tomli-2.3.0.tar.gz", hash = "sha256:64be704a875d2a59753d80ee8a533c3fe183e3f06807ff7dc2232938ccb01549", size = 17392, upload-time = "2025-10-08T22:01:47.119Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b3/2e/299f62b401438d5fe1624119c723f5d877acc86a4c2492da405626665f12/tomli-2.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:88bd15eb972f3664f5ed4b57c1634a97153b4bac4479dcb6a495f41921eb7f45", size = 153236, upload-time = "2025-10-08T22:01:00.137Z" },
{ url = "https://files.pythonhosted.org/packages/86/7f/d8fffe6a7aefdb61bced88fcb5e280cfd71e08939da5894161bd71bea022/tomli-2.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:883b1c0d6398a6a9d29b508c331fa56adbcdff647f6ace4dfca0f50e90dfd0ba", size = 148084, upload-time = "2025-10-08T22:01:01.63Z" },
{ url = "https://files.pythonhosted.org/packages/47/5c/24935fb6a2ee63e86d80e4d3b58b222dafaf438c416752c8b58537c8b89a/tomli-2.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1381caf13ab9f300e30dd8feadb3de072aeb86f1d34a8569453ff32a7dea4bf", size = 234832, upload-time = "2025-10-08T22:01:02.543Z" },
{ url = "https://files.pythonhosted.org/packages/89/da/75dfd804fc11e6612846758a23f13271b76d577e299592b4371a4ca4cd09/tomli-2.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0e285d2649b78c0d9027570d4da3425bdb49830a6156121360b3f8511ea3441", size = 242052, upload-time = "2025-10-08T22:01:03.836Z" },
{ url = "https://files.pythonhosted.org/packages/70/8c/f48ac899f7b3ca7eb13af73bacbc93aec37f9c954df3c08ad96991c8c373/tomli-2.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0a154a9ae14bfcf5d8917a59b51ffd5a3ac1fd149b71b47a3a104ca4edcfa845", size = 239555, upload-time = "2025-10-08T22:01:04.834Z" },
{ url = "https://files.pythonhosted.org/packages/ba/28/72f8afd73f1d0e7829bfc093f4cb98ce0a40ffc0cc997009ee1ed94ba705/tomli-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:74bf8464ff93e413514fefd2be591c3b0b23231a77f901db1eb30d6f712fc42c", size = 245128, upload-time = "2025-10-08T22:01:05.84Z" },
{ url = "https://files.pythonhosted.org/packages/b6/eb/a7679c8ac85208706d27436e8d421dfa39d4c914dcf5fa8083a9305f58d9/tomli-2.3.0-cp311-cp311-win32.whl", hash = "sha256:00b5f5d95bbfc7d12f91ad8c593a1659b6387b43f054104cda404be6bda62456", size = 96445, upload-time = "2025-10-08T22:01:06.896Z" },
{ url = "https://files.pythonhosted.org/packages/0a/fe/3d3420c4cb1ad9cb462fb52967080575f15898da97e21cb6f1361d505383/tomli-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:4dc4ce8483a5d429ab602f111a93a6ab1ed425eae3122032db7e9acf449451be", size = 107165, upload-time = "2025-10-08T22:01:08.107Z" },
{ url = "https://files.pythonhosted.org/packages/ff/b7/40f36368fcabc518bb11c8f06379a0fd631985046c038aca08c6d6a43c6e/tomli-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d7d86942e56ded512a594786a5ba0a5e521d02529b3826e7761a05138341a2ac", size = 154891, upload-time = "2025-10-08T22:01:09.082Z" },
{ url = "https://files.pythonhosted.org/packages/f9/3f/d9dd692199e3b3aab2e4e4dd948abd0f790d9ded8cd10cbaae276a898434/tomli-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:73ee0b47d4dad1c5e996e3cd33b8a76a50167ae5f96a2607cbe8cc773506ab22", size = 148796, upload-time = "2025-10-08T22:01:10.266Z" },
{ url = "https://files.pythonhosted.org/packages/60/83/59bff4996c2cf9f9387a0f5a3394629c7efa5ef16142076a23a90f1955fa/tomli-2.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:792262b94d5d0a466afb5bc63c7daa9d75520110971ee269152083270998316f", size = 242121, upload-time = "2025-10-08T22:01:11.332Z" },
{ url = "https://files.pythonhosted.org/packages/45/e5/7c5119ff39de8693d6baab6c0b6dcb556d192c165596e9fc231ea1052041/tomli-2.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f195fe57ecceac95a66a75ac24d9d5fbc98ef0962e09b2eddec5d39375aae52", size = 250070, upload-time = "2025-10-08T22:01:12.498Z" },
{ url = "https://files.pythonhosted.org/packages/45/12/ad5126d3a278f27e6701abde51d342aa78d06e27ce2bb596a01f7709a5a2/tomli-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e31d432427dcbf4d86958c184b9bfd1e96b5b71f8eb17e6d02531f434fd335b8", size = 245859, upload-time = "2025-10-08T22:01:13.551Z" },
{ url = "https://files.pythonhosted.org/packages/fb/a1/4d6865da6a71c603cfe6ad0e6556c73c76548557a8d658f9e3b142df245f/tomli-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b0882799624980785240ab732537fcfc372601015c00f7fc367c55308c186f6", size = 250296, upload-time = "2025-10-08T22:01:14.614Z" },
{ url = "https://files.pythonhosted.org/packages/a0/b7/a7a7042715d55c9ba6e8b196d65d2cb662578b4d8cd17d882d45322b0d78/tomli-2.3.0-cp312-cp312-win32.whl", hash = "sha256:ff72b71b5d10d22ecb084d345fc26f42b5143c5533db5e2eaba7d2d335358876", size = 97124, upload-time = "2025-10-08T22:01:15.629Z" },
{ url = "https://files.pythonhosted.org/packages/06/1e/f22f100db15a68b520664eb3328fb0ae4e90530887928558112c8d1f4515/tomli-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:1cb4ed918939151a03f33d4242ccd0aa5f11b3547d0cf30f7c74a408a5b99878", size = 107698, upload-time = "2025-10-08T22:01:16.51Z" },
{ url = "https://files.pythonhosted.org/packages/89/48/06ee6eabe4fdd9ecd48bf488f4ac783844fd777f547b8d1b61c11939974e/tomli-2.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5192f562738228945d7b13d4930baffda67b69425a7f0da96d360b0a3888136b", size = 154819, upload-time = "2025-10-08T22:01:17.964Z" },
{ url = "https://files.pythonhosted.org/packages/f1/01/88793757d54d8937015c75dcdfb673c65471945f6be98e6a0410fba167ed/tomli-2.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:be71c93a63d738597996be9528f4abe628d1adf5e6eb11607bc8fe1a510b5dae", size = 148766, upload-time = "2025-10-08T22:01:18.959Z" },
{ url = "https://files.pythonhosted.org/packages/42/17/5e2c956f0144b812e7e107f94f1cc54af734eb17b5191c0bbfb72de5e93e/tomli-2.3.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4665508bcbac83a31ff8ab08f424b665200c0e1e645d2bd9ab3d3e557b6185b", size = 240771, upload-time = "2025-10-08T22:01:20.106Z" },
{ url = "https://files.pythonhosted.org/packages/d5/f4/0fbd014909748706c01d16824eadb0307115f9562a15cbb012cd9b3512c5/tomli-2.3.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4021923f97266babc6ccab9f5068642a0095faa0a51a246a6a02fccbb3514eaf", size = 248586, upload-time = "2025-10-08T22:01:21.164Z" },
{ url = "https://files.pythonhosted.org/packages/30/77/fed85e114bde5e81ecf9bc5da0cc69f2914b38f4708c80ae67d0c10180c5/tomli-2.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4ea38c40145a357d513bffad0ed869f13c1773716cf71ccaa83b0fa0cc4e42f", size = 244792, upload-time = "2025-10-08T22:01:22.417Z" },
{ url = "https://files.pythonhosted.org/packages/55/92/afed3d497f7c186dc71e6ee6d4fcb0acfa5f7d0a1a2878f8beae379ae0cc/tomli-2.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ad805ea85eda330dbad64c7ea7a4556259665bdf9d2672f5dccc740eb9d3ca05", size = 248909, upload-time = "2025-10-08T22:01:23.859Z" },
{ url = "https://files.pythonhosted.org/packages/f8/84/ef50c51b5a9472e7265ce1ffc7f24cd4023d289e109f669bdb1553f6a7c2/tomli-2.3.0-cp313-cp313-win32.whl", hash = "sha256:97d5eec30149fd3294270e889b4234023f2c69747e555a27bd708828353ab606", size = 96946, upload-time = "2025-10-08T22:01:24.893Z" },
{ url = "https://files.pythonhosted.org/packages/b2/b7/718cd1da0884f281f95ccfa3a6cc572d30053cba64603f79d431d3c9b61b/tomli-2.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:0c95ca56fbe89e065c6ead5b593ee64b84a26fca063b5d71a1122bf26e533999", size = 107705, upload-time = "2025-10-08T22:01:26.153Z" },
{ url = "https://files.pythonhosted.org/packages/19/94/aeafa14a52e16163008060506fcb6aa1949d13548d13752171a755c65611/tomli-2.3.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:cebc6fe843e0733ee827a282aca4999b596241195f43b4cc371d64fc6639da9e", size = 154244, upload-time = "2025-10-08T22:01:27.06Z" },
{ url = "https://files.pythonhosted.org/packages/db/e4/1e58409aa78eefa47ccd19779fc6f36787edbe7d4cd330eeeedb33a4515b/tomli-2.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4c2ef0244c75aba9355561272009d934953817c49f47d768070c3c94355c2aa3", size = 148637, upload-time = "2025-10-08T22:01:28.059Z" },
{ url = "https://files.pythonhosted.org/packages/26/b6/d1eccb62f665e44359226811064596dd6a366ea1f985839c566cd61525ae/tomli-2.3.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c22a8bf253bacc0cf11f35ad9808b6cb75ada2631c2d97c971122583b129afbc", size = 241925, upload-time = "2025-10-08T22:01:29.066Z" },
{ url = "https://files.pythonhosted.org/packages/70/91/7cdab9a03e6d3d2bb11beae108da5bdc1c34bdeb06e21163482544ddcc90/tomli-2.3.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0eea8cc5c5e9f89c9b90c4896a8deefc74f518db5927d0e0e8d4a80953d774d0", size = 249045, upload-time = "2025-10-08T22:01:31.98Z" },
{ url = "https://files.pythonhosted.org/packages/15/1b/8c26874ed1f6e4f1fcfeb868db8a794cbe9f227299402db58cfcc858766c/tomli-2.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b74a0e59ec5d15127acdabd75ea17726ac4c5178ae51b85bfe39c4f8a278e879", size = 245835, upload-time = "2025-10-08T22:01:32.989Z" },
{ url = "https://files.pythonhosted.org/packages/fd/42/8e3c6a9a4b1a1360c1a2a39f0b972cef2cc9ebd56025168c4137192a9321/tomli-2.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:b5870b50c9db823c595983571d1296a6ff3e1b88f734a4c8f6fc6188397de005", size = 253109, upload-time = "2025-10-08T22:01:34.052Z" },
{ url = "https://files.pythonhosted.org/packages/22/0c/b4da635000a71b5f80130937eeac12e686eefb376b8dee113b4a582bba42/tomli-2.3.0-cp314-cp314-win32.whl", hash = "sha256:feb0dacc61170ed7ab602d3d972a58f14ee3ee60494292d384649a3dc38ef463", size = 97930, upload-time = "2025-10-08T22:01:35.082Z" },
{ url = "https://files.pythonhosted.org/packages/b9/74/cb1abc870a418ae99cd5c9547d6bce30701a954e0e721821df483ef7223c/tomli-2.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:b273fcbd7fc64dc3600c098e39136522650c49bca95df2d11cf3b626422392c8", size = 107964, upload-time = "2025-10-08T22:01:36.057Z" },
{ url = "https://files.pythonhosted.org/packages/54/78/5c46fff6432a712af9f792944f4fcd7067d8823157949f4e40c56b8b3c83/tomli-2.3.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:940d56ee0410fa17ee1f12b817b37a4d4e4dc4d27340863cc67236c74f582e77", size = 163065, upload-time = "2025-10-08T22:01:37.27Z" },
{ url = "https://files.pythonhosted.org/packages/39/67/f85d9bd23182f45eca8939cd2bc7050e1f90c41f4a2ecbbd5963a1d1c486/tomli-2.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f85209946d1fe94416debbb88d00eb92ce9cd5266775424ff81bc959e001acaf", size = 159088, upload-time = "2025-10-08T22:01:38.235Z" },
{ url = "https://files.pythonhosted.org/packages/26/5a/4b546a0405b9cc0659b399f12b6adb750757baf04250b148d3c5059fc4eb/tomli-2.3.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a56212bdcce682e56b0aaf79e869ba5d15a6163f88d5451cbde388d48b13f530", size = 268193, upload-time = "2025-10-08T22:01:39.712Z" },
{ url = "https://files.pythonhosted.org/packages/42/4f/2c12a72ae22cf7b59a7fe75b3465b7aba40ea9145d026ba41cb382075b0e/tomli-2.3.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c5f3ffd1e098dfc032d4d3af5c0ac64f6d286d98bc148698356847b80fa4de1b", size = 275488, upload-time = "2025-10-08T22:01:40.773Z" },
{ url = "https://files.pythonhosted.org/packages/92/04/a038d65dbe160c3aa5a624e93ad98111090f6804027d474ba9c37c8ae186/tomli-2.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5e01decd096b1530d97d5d85cb4dff4af2d8347bd35686654a004f8dea20fc67", size = 272669, upload-time = "2025-10-08T22:01:41.824Z" },
{ url = "https://files.pythonhosted.org/packages/be/2f/8b7c60a9d1612a7cbc39ffcca4f21a73bf368a80fc25bccf8253e2563267/tomli-2.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8a35dd0e643bb2610f156cca8db95d213a90015c11fee76c946aa62b7ae7e02f", size = 279709, upload-time = "2025-10-08T22:01:43.177Z" },
{ url = "https://files.pythonhosted.org/packages/7e/46/cc36c679f09f27ded940281c38607716c86cf8ba4a518d524e349c8b4874/tomli-2.3.0-cp314-cp314t-win32.whl", hash = "sha256:a1f7f282fe248311650081faafa5f4732bdbfef5d45fe3f2e702fbc6f2d496e0", size = 107563, upload-time = "2025-10-08T22:01:44.233Z" },
{ url = "https://files.pythonhosted.org/packages/84/ff/426ca8683cf7b753614480484f6437f568fd2fda2edbdf57a2d3d8b27a0b/tomli-2.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:70a251f8d4ba2d9ac2542eecf008b3c8a9fc5c3f9f02c56a9d7952612be2fdba", size = 119756, upload-time = "2025-10-08T22:01:45.234Z" },
{ url = "https://files.pythonhosted.org/packages/77/b8/0135fadc89e73be292b473cb820b4f5a08197779206b33191e801feeae40/tomli-2.3.0-py3-none-any.whl", hash = "sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b", size = 14408, upload-time = "2025-10-08T22:01:46.04Z" },
]
[[package]]
name = "typing-extensions"
version = "4.15.0"
@@ -1068,6 +1313,18 @@ version = "0.22.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/06/f0/18d39dbd1971d6d62c4629cc7fa67f74821b0dc1f5a77af43719de7936a7/uvloop-0.22.1.tar.gz", hash = "sha256:6c84bae345b9147082b17371e3dd5d42775bddce91f885499017f4607fdaf39f", size = 2443250, upload-time = "2025-10-16T22:17:19.342Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c7/d5/69900f7883235562f1f50d8184bb7dd84a2fb61e9ec63f3782546fdbd057/uvloop-0.22.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c60ebcd36f7b240b30788554b6f0782454826a0ed765d8430652621b5de674b9", size = 1352420, upload-time = "2025-10-16T22:16:21.187Z" },
{ url = "https://files.pythonhosted.org/packages/a8/73/c4e271b3bce59724e291465cc936c37758886a4868787da0278b3b56b905/uvloop-0.22.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3b7f102bf3cb1995cfeaee9321105e8f5da76fdb104cdad8986f85461a1b7b77", size = 748677, upload-time = "2025-10-16T22:16:22.558Z" },
{ url = "https://files.pythonhosted.org/packages/86/94/9fb7fad2f824d25f8ecac0d70b94d0d48107ad5ece03769a9c543444f78a/uvloop-0.22.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53c85520781d84a4b8b230e24a5af5b0778efdb39142b424990ff1ef7c48ba21", size = 3753819, upload-time = "2025-10-16T22:16:23.903Z" },
{ url = "https://files.pythonhosted.org/packages/74/4f/256aca690709e9b008b7108bc85fba619a2bc37c6d80743d18abad16ee09/uvloop-0.22.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:56a2d1fae65fd82197cb8c53c367310b3eabe1bbb9fb5a04d28e3e3520e4f702", size = 3804529, upload-time = "2025-10-16T22:16:25.246Z" },
{ url = "https://files.pythonhosted.org/packages/7f/74/03c05ae4737e871923d21a76fe28b6aad57f5c03b6e6bfcfa5ad616013e4/uvloop-0.22.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:40631b049d5972c6755b06d0bfe8233b1bd9a8a6392d9d1c45c10b6f9e9b2733", size = 3621267, upload-time = "2025-10-16T22:16:26.819Z" },
{ url = "https://files.pythonhosted.org/packages/75/be/f8e590fe61d18b4a92070905497aec4c0e64ae1761498cad09023f3f4b3e/uvloop-0.22.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:535cc37b3a04f6cd2c1ef65fa1d370c9a35b6695df735fcff5427323f2cd5473", size = 3723105, upload-time = "2025-10-16T22:16:28.252Z" },
{ url = "https://files.pythonhosted.org/packages/3d/ff/7f72e8170be527b4977b033239a83a68d5c881cc4775fca255c677f7ac5d/uvloop-0.22.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:fe94b4564e865d968414598eea1a6de60adba0c040ba4ed05ac1300de402cd42", size = 1359936, upload-time = "2025-10-16T22:16:29.436Z" },
{ url = "https://files.pythonhosted.org/packages/c3/c6/e5d433f88fd54d81ef4be58b2b7b0cea13c442454a1db703a1eea0db1a59/uvloop-0.22.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:51eb9bd88391483410daad430813d982010f9c9c89512321f5b60e2cddbdddd6", size = 752769, upload-time = "2025-10-16T22:16:30.493Z" },
{ url = "https://files.pythonhosted.org/packages/24/68/a6ac446820273e71aa762fa21cdcc09861edd3536ff47c5cd3b7afb10eeb/uvloop-0.22.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:700e674a166ca5778255e0e1dc4e9d79ab2acc57b9171b79e65feba7184b3370", size = 4317413, upload-time = "2025-10-16T22:16:31.644Z" },
{ url = "https://files.pythonhosted.org/packages/5f/6f/e62b4dfc7ad6518e7eff2516f680d02a0f6eb62c0c212e152ca708a0085e/uvloop-0.22.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7b5b1ac819a3f946d3b2ee07f09149578ae76066d70b44df3fa990add49a82e4", size = 4426307, upload-time = "2025-10-16T22:16:32.917Z" },
{ url = "https://files.pythonhosted.org/packages/90/60/97362554ac21e20e81bcef1150cb2a7e4ffdaf8ea1e5b2e8bf7a053caa18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e047cc068570bac9866237739607d1313b9253c3051ad84738cbb095be0537b2", size = 4131970, upload-time = "2025-10-16T22:16:34.015Z" },
{ url = "https://files.pythonhosted.org/packages/99/39/6b3f7d234ba3964c428a6e40006340f53ba37993f46ed6e111c6e9141d18/uvloop-0.22.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:512fec6815e2dd45161054592441ef76c830eddaad55c8aa30952e6fe1ed07c0", size = 4296343, upload-time = "2025-10-16T22:16:35.149Z" },
{ url = "https://files.pythonhosted.org/packages/89/8c/182a2a593195bfd39842ea68ebc084e20c850806117213f5a299dfc513d9/uvloop-0.22.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:561577354eb94200d75aca23fbde86ee11be36b00e52a4eaf8f50fb0c86b7705", size = 1358611, upload-time = "2025-10-16T22:16:36.833Z" },
{ url = "https://files.pythonhosted.org/packages/d2/14/e301ee96a6dc95224b6f1162cd3312f6d1217be3907b79173b06785f2fe7/uvloop-0.22.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cdf5192ab3e674ca26da2eada35b288d2fa49fdd0f357a19f0e7c4e7d5077c8", size = 751811, upload-time = "2025-10-16T22:16:38.275Z" },
{ url = "https://files.pythonhosted.org/packages/b7/02/654426ce265ac19e2980bfd9ea6590ca96a56f10c76e63801a2df01c0486/uvloop-0.22.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e2ea3d6190a2968f4a14a23019d3b16870dd2190cd69c8180f7c632d21de68d", size = 4288562, upload-time = "2025-10-16T22:16:39.375Z" },
@@ -1094,6 +1351,12 @@ version = "4.0.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/4f/38/764baaa25eb5e35c9a043d4c4588f9836edfe52a708950f4b6d5f714fd42/watchdog-4.0.2.tar.gz", hash = "sha256:b4dfbb6c49221be4535623ea4474a4d6ee0a9cef4a80b20c28db4d858b64e270", size = 126587, upload-time = "2024-08-11T07:38:01.623Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/de/78/027ad372d62f97642349a16015394a7680530460b1c70c368c506cb60c09/watchdog-4.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7c7d4bf585ad501c5f6c980e7be9c4f15604c7cc150e942d82083b31a7548930", size = 100256, upload-time = "2024-08-11T07:37:11.017Z" },
{ url = "https://files.pythonhosted.org/packages/59/a9/412b808568c1814d693b4ff1cec0055dc791780b9dc947807978fab86bc1/watchdog-4.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:914285126ad0b6eb2258bbbcb7b288d9dfd655ae88fa28945be05a7b475a800b", size = 92252, upload-time = "2024-08-11T07:37:13.098Z" },
{ url = "https://files.pythonhosted.org/packages/04/57/179d76076cff264982bc335dd4c7da6d636bd3e9860bbc896a665c3447b6/watchdog-4.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:984306dc4720da5498b16fc037b36ac443816125a3705dfde4fd90652d8028ef", size = 92888, upload-time = "2024-08-11T07:37:15.077Z" },
{ url = "https://files.pythonhosted.org/packages/92/f5/ea22b095340545faea37ad9a42353b265ca751f543da3fb43f5d00cdcd21/watchdog-4.0.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:1cdcfd8142f604630deef34722d695fb455d04ab7cfe9963055df1fc69e6727a", size = 100342, upload-time = "2024-08-11T07:37:16.393Z" },
{ url = "https://files.pythonhosted.org/packages/cb/d2/8ce97dff5e465db1222951434e3115189ae54a9863aef99c6987890cc9ef/watchdog-4.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d7ab624ff2f663f98cd03c8b7eedc09375a911794dfea6bf2a359fcc266bff29", size = 92306, upload-time = "2024-08-11T07:37:17.997Z" },
{ url = "https://files.pythonhosted.org/packages/49/c4/1aeba2c31b25f79b03b15918155bc8c0b08101054fc727900f1a577d0d54/watchdog-4.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:132937547a716027bd5714383dfc40dc66c26769f1ce8a72a859d6a48f371f3a", size = 92915, upload-time = "2024-08-11T07:37:19.967Z" },
{ url = "https://files.pythonhosted.org/packages/79/63/eb8994a182672c042d85a33507475c50c2ee930577524dd97aea05251527/watchdog-4.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:cd67c7df93eb58f360c43802acc945fa8da70c675b6fa37a241e17ca698ca49b", size = 100343, upload-time = "2024-08-11T07:37:21.935Z" },
{ url = "https://files.pythonhosted.org/packages/ce/82/027c0c65c2245769580605bcd20a1dc7dfd6c6683c8c4e2ef43920e38d27/watchdog-4.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:bcfd02377be80ef3b6bc4ce481ef3959640458d6feaae0bd43dd90a43da90a7d", size = 92313, upload-time = "2024-08-11T07:37:23.314Z" },
{ url = "https://files.pythonhosted.org/packages/2a/89/ad4715cbbd3440cb0d336b78970aba243a33a24b1a79d66f8d16b4590d6a/watchdog-4.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:980b71510f59c884d684b3663d46e7a14b457c9611c481e5cef08f4dd022eed7", size = 92919, upload-time = "2024-08-11T07:37:24.715Z" },
@@ -1118,6 +1381,32 @@ dependencies = [
]
sdist = { url = "https://files.pythonhosted.org/packages/c2/c9/8869df9b2a2d6c59d79220a4db37679e74f807c559ffe5265e08b227a210/watchfiles-1.1.1.tar.gz", hash = "sha256:a173cb5c16c4f40ab19cecf48a534c409f7ea983ab8fed0741304a1c0a31b3f2", size = 94440, upload-time = "2025-10-14T15:06:21.08Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/1f/f8/2c5f479fb531ce2f0564eda479faecf253d886b1ab3630a39b7bf7362d46/watchfiles-1.1.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:f57b396167a2565a4e8b5e56a5a1c537571733992b226f4f1197d79e94cf0ae5", size = 406529, upload-time = "2025-10-14T15:04:32.899Z" },
{ url = "https://files.pythonhosted.org/packages/fe/cd/f515660b1f32f65df671ddf6f85bfaca621aee177712874dc30a97397977/watchfiles-1.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:421e29339983e1bebc281fab40d812742268ad057db4aee8c4d2bce0af43b741", size = 394384, upload-time = "2025-10-14T15:04:33.761Z" },
{ url = "https://files.pythonhosted.org/packages/7b/c3/28b7dc99733eab43fca2d10f55c86e03bd6ab11ca31b802abac26b23d161/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e43d39a741e972bab5d8100b5cdacf69db64e34eb19b6e9af162bccf63c5cc6", size = 448789, upload-time = "2025-10-14T15:04:34.679Z" },
{ url = "https://files.pythonhosted.org/packages/4a/24/33e71113b320030011c8e4316ccca04194bf0cbbaeee207f00cbc7d6b9f5/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f537afb3276d12814082a2e9b242bdcf416c2e8fd9f799a737990a1dbe906e5b", size = 460521, upload-time = "2025-10-14T15:04:35.963Z" },
{ url = "https://files.pythonhosted.org/packages/f4/c3/3c9a55f255aa57b91579ae9e98c88704955fa9dac3e5614fb378291155df/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b2cd9e04277e756a2e2d2543d65d1e2166d6fd4c9b183f8808634fda23f17b14", size = 488722, upload-time = "2025-10-14T15:04:37.091Z" },
{ url = "https://files.pythonhosted.org/packages/49/36/506447b73eb46c120169dc1717fe2eff07c234bb3232a7200b5f5bd816e9/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f3f58818dc0b07f7d9aa7fe9eb1037aecb9700e63e1f6acfed13e9fef648f5d", size = 596088, upload-time = "2025-10-14T15:04:38.39Z" },
{ url = "https://files.pythonhosted.org/packages/82/ab/5f39e752a9838ec4d52e9b87c1e80f1ee3ccdbe92e183c15b6577ab9de16/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9bb9f66367023ae783551042d31b1d7fd422e8289eedd91f26754a66f44d5cff", size = 472923, upload-time = "2025-10-14T15:04:39.666Z" },
{ url = "https://files.pythonhosted.org/packages/af/b9/a419292f05e302dea372fa7e6fda5178a92998411f8581b9830d28fb9edb/watchfiles-1.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aebfd0861a83e6c3d1110b78ad54704486555246e542be3e2bb94195eabb2606", size = 456080, upload-time = "2025-10-14T15:04:40.643Z" },
{ url = "https://files.pythonhosted.org/packages/b0/c3/d5932fd62bde1a30c36e10c409dc5d54506726f08cb3e1d8d0ba5e2bc8db/watchfiles-1.1.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:5fac835b4ab3c6487b5dbad78c4b3724e26bcc468e886f8ba8cc4306f68f6701", size = 629432, upload-time = "2025-10-14T15:04:41.789Z" },
{ url = "https://files.pythonhosted.org/packages/f7/77/16bddd9779fafb795f1a94319dc965209c5641db5bf1edbbccace6d1b3c0/watchfiles-1.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:399600947b170270e80134ac854e21b3ccdefa11a9529a3decc1327088180f10", size = 623046, upload-time = "2025-10-14T15:04:42.718Z" },
{ url = "https://files.pythonhosted.org/packages/46/ef/f2ecb9a0f342b4bfad13a2787155c6ee7ce792140eac63a34676a2feeef2/watchfiles-1.1.1-cp311-cp311-win32.whl", hash = "sha256:de6da501c883f58ad50db3a32ad397b09ad29865b5f26f64c24d3e3281685849", size = 271473, upload-time = "2025-10-14T15:04:43.624Z" },
{ url = "https://files.pythonhosted.org/packages/94/bc/f42d71125f19731ea435c3948cad148d31a64fccde3867e5ba4edee901f9/watchfiles-1.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:35c53bd62a0b885bf653ebf6b700d1bf05debb78ad9292cf2a942b23513dc4c4", size = 287598, upload-time = "2025-10-14T15:04:44.516Z" },
{ url = "https://files.pythonhosted.org/packages/57/c9/a30f897351f95bbbfb6abcadafbaca711ce1162f4db95fc908c98a9165f3/watchfiles-1.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:57ca5281a8b5e27593cb7d82c2ac927ad88a96ed406aa446f6344e4328208e9e", size = 277210, upload-time = "2025-10-14T15:04:45.883Z" },
{ url = "https://files.pythonhosted.org/packages/74/d5/f039e7e3c639d9b1d09b07ea412a6806d38123f0508e5f9b48a87b0a76cc/watchfiles-1.1.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:8c89f9f2f740a6b7dcc753140dd5e1ab9215966f7a3530d0c0705c83b401bd7d", size = 404745, upload-time = "2025-10-14T15:04:46.731Z" },
{ url = "https://files.pythonhosted.org/packages/a5/96/a881a13aa1349827490dab2d363c8039527060cfcc2c92cc6d13d1b1049e/watchfiles-1.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bd404be08018c37350f0d6e34676bd1e2889990117a2b90070b3007f172d0610", size = 391769, upload-time = "2025-10-14T15:04:48.003Z" },
{ url = "https://files.pythonhosted.org/packages/4b/5b/d3b460364aeb8da471c1989238ea0e56bec24b6042a68046adf3d9ddb01c/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8526e8f916bb5b9a0a777c8317c23ce65de259422bba5b31325a6fa6029d33af", size = 449374, upload-time = "2025-10-14T15:04:49.179Z" },
{ url = "https://files.pythonhosted.org/packages/b9/44/5769cb62d4ed055cb17417c0a109a92f007114a4e07f30812a73a4efdb11/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2edc3553362b1c38d9f06242416a5d8e9fe235c204a4072e988ce2e5bb1f69f6", size = 459485, upload-time = "2025-10-14T15:04:50.155Z" },
{ url = "https://files.pythonhosted.org/packages/19/0c/286b6301ded2eccd4ffd0041a1b726afda999926cf720aab63adb68a1e36/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30f7da3fb3f2844259cba4720c3fc7138eb0f7b659c38f3bfa65084c7fc7abce", size = 488813, upload-time = "2025-10-14T15:04:51.059Z" },
{ url = "https://files.pythonhosted.org/packages/c7/2b/8530ed41112dd4a22f4dcfdb5ccf6a1baad1ff6eed8dc5a5f09e7e8c41c7/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f8979280bdafff686ba5e4d8f97840f929a87ed9cdf133cbbd42f7766774d2aa", size = 594816, upload-time = "2025-10-14T15:04:52.031Z" },
{ url = "https://files.pythonhosted.org/packages/ce/d2/f5f9fb49489f184f18470d4f99f4e862a4b3e9ac2865688eb2099e3d837a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dcc5c24523771db3a294c77d94771abcfcb82a0e0ee8efd910c37c59ec1b31bb", size = 475186, upload-time = "2025-10-14T15:04:53.064Z" },
{ url = "https://files.pythonhosted.org/packages/cf/68/5707da262a119fb06fbe214d82dd1fe4a6f4af32d2d14de368d0349eb52a/watchfiles-1.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1db5d7ae38ff20153d542460752ff397fcf5c96090c1230803713cf3147a6803", size = 456812, upload-time = "2025-10-14T15:04:55.174Z" },
{ url = "https://files.pythonhosted.org/packages/66/ab/3cbb8756323e8f9b6f9acb9ef4ec26d42b2109bce830cc1f3468df20511d/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:28475ddbde92df1874b6c5c8aaeb24ad5be47a11f87cde5a28ef3835932e3e94", size = 630196, upload-time = "2025-10-14T15:04:56.22Z" },
{ url = "https://files.pythonhosted.org/packages/78/46/7152ec29b8335f80167928944a94955015a345440f524d2dfe63fc2f437b/watchfiles-1.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:36193ed342f5b9842edd3532729a2ad55c4160ffcfa3700e0d54be496b70dd43", size = 622657, upload-time = "2025-10-14T15:04:57.521Z" },
{ url = "https://files.pythonhosted.org/packages/0a/bf/95895e78dd75efe9a7f31733607f384b42eb5feb54bd2eb6ed57cc2e94f4/watchfiles-1.1.1-cp312-cp312-win32.whl", hash = "sha256:859e43a1951717cc8de7f4c77674a6d389b106361585951d9e69572823f311d9", size = 272042, upload-time = "2025-10-14T15:04:59.046Z" },
{ url = "https://files.pythonhosted.org/packages/87/0a/90eb755f568de2688cb220171c4191df932232c20946966c27a59c400850/watchfiles-1.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:91d4c9a823a8c987cce8fa2690923b069966dabb196dd8d137ea2cede885fde9", size = 288410, upload-time = "2025-10-14T15:05:00.081Z" },
{ url = "https://files.pythonhosted.org/packages/36/76/f322701530586922fbd6723c4f91ace21364924822a8772c549483abed13/watchfiles-1.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:a625815d4a2bdca61953dbba5a39d60164451ef34c88d751f6c368c3ea73d404", size = 278209, upload-time = "2025-10-14T15:05:01.168Z" },
{ url = "https://files.pythonhosted.org/packages/bb/f4/f750b29225fe77139f7ae5de89d4949f5a99f934c65a1f1c0b248f26f747/watchfiles-1.1.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:130e4876309e8686a5e37dba7d5e9bc77e6ed908266996ca26572437a5271e18", size = 404321, upload-time = "2025-10-14T15:05:02.063Z" },
{ url = "https://files.pythonhosted.org/packages/2b/f9/f07a295cde762644aa4c4bb0f88921d2d141af45e735b965fb2e87858328/watchfiles-1.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5f3bde70f157f84ece3765b42b4a52c6ac1a50334903c6eaf765362f6ccca88a", size = 391783, upload-time = "2025-10-14T15:05:03.052Z" },
{ url = "https://files.pythonhosted.org/packages/bc/11/fc2502457e0bea39a5c958d86d2cb69e407a4d00b85735ca724bfa6e0d1a/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14e0b1fe858430fc0251737ef3824c54027bedb8c37c38114488b8e131cf8219", size = 449279, upload-time = "2025-10-14T15:05:04.004Z" },
@@ -1164,6 +1453,10 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/4f/55/2af26693fd15165c4ff7857e38330e1b61ab8c37d15dc79118cdba115b7a/watchfiles-1.1.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c91ed27800188c2ae96d16e3149f199d62f86c7af5f5f4d2c61a3ed8cd3666c", size = 455072, upload-time = "2025-10-14T15:05:48.928Z" },
{ url = "https://files.pythonhosted.org/packages/66/1d/d0d200b10c9311ec25d2273f8aad8c3ef7cc7ea11808022501811208a750/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:311ff15a0bae3714ffb603e6ba6dbfba4065ab60865d15a6ec544133bdb21099", size = 629104, upload-time = "2025-10-14T15:05:49.908Z" },
{ url = "https://files.pythonhosted.org/packages/e3/bd/fa9bb053192491b3867ba07d2343d9f2252e00811567d30ae8d0f78136fe/watchfiles-1.1.1-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:a916a2932da8f8ab582f242c065f5c81bed3462849ca79ee357dd9551b0e9b01", size = 622112, upload-time = "2025-10-14T15:05:50.941Z" },
{ url = "https://files.pythonhosted.org/packages/d3/8e/e500f8b0b77be4ff753ac94dc06b33d8f0d839377fee1b78e8c8d8f031bf/watchfiles-1.1.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:db476ab59b6765134de1d4fe96a1a9c96ddf091683599be0f26147ea1b2e4b88", size = 408250, upload-time = "2025-10-14T15:06:10.264Z" },
{ url = "https://files.pythonhosted.org/packages/bd/95/615e72cd27b85b61eec764a5ca51bd94d40b5adea5ff47567d9ebc4d275a/watchfiles-1.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:89eef07eee5e9d1fda06e38822ad167a044153457e6fd997f8a858ab7564a336", size = 396117, upload-time = "2025-10-14T15:06:11.28Z" },
{ url = "https://files.pythonhosted.org/packages/c9/81/e7fe958ce8a7fb5c73cc9fb07f5aeaf755e6aa72498c57d760af760c91f8/watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce19e06cbda693e9e7686358af9cd6f5d61312ab8b00488bc36f5aabbaf77e24", size = 450493, upload-time = "2025-10-14T15:06:12.321Z" },
{ url = "https://files.pythonhosted.org/packages/6e/d4/ed38dd3b1767193de971e694aa544356e63353c33a85d948166b5ff58b9e/watchfiles-1.1.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e6f39af2eab0118338902798b5aa6664f46ff66bc0280de76fca67a7f262a49", size = 457546, upload-time = "2025-10-14T15:06:13.372Z" },
]
[[package]]
@@ -1172,6 +1465,28 @@ version = "15.0.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016, upload-time = "2025-03-05T20:03:41.606Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/9f/32/18fcd5919c293a398db67443acd33fde142f283853076049824fc58e6f75/websockets-15.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:823c248b690b2fd9303ba00c4f66cd5e2d8c3ba4aa968b2779be9532a4dad431", size = 175423, upload-time = "2025-03-05T20:01:56.276Z" },
{ url = "https://files.pythonhosted.org/packages/76/70/ba1ad96b07869275ef42e2ce21f07a5b0148936688c2baf7e4a1f60d5058/websockets-15.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678999709e68425ae2593acf2e3ebcbcf2e69885a5ee78f9eb80e6e371f1bf57", size = 173082, upload-time = "2025-03-05T20:01:57.563Z" },
{ url = "https://files.pythonhosted.org/packages/86/f2/10b55821dd40eb696ce4704a87d57774696f9451108cff0d2824c97e0f97/websockets-15.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d50fd1ee42388dcfb2b3676132c78116490976f1300da28eb629272d5d93e905", size = 173330, upload-time = "2025-03-05T20:01:59.063Z" },
{ url = "https://files.pythonhosted.org/packages/a5/90/1c37ae8b8a113d3daf1065222b6af61cc44102da95388ac0018fcb7d93d9/websockets-15.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d99e5546bf73dbad5bf3547174cd6cb8ba7273062a23808ffea025ecb1cf8562", size = 182878, upload-time = "2025-03-05T20:02:00.305Z" },
{ url = "https://files.pythonhosted.org/packages/8e/8d/96e8e288b2a41dffafb78e8904ea7367ee4f891dafc2ab8d87e2124cb3d3/websockets-15.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66dd88c918e3287efc22409d426c8f729688d89a0c587c88971a0faa2c2f3792", size = 181883, upload-time = "2025-03-05T20:02:03.148Z" },
{ url = "https://files.pythonhosted.org/packages/93/1f/5d6dbf551766308f6f50f8baf8e9860be6182911e8106da7a7f73785f4c4/websockets-15.0.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dd8327c795b3e3f219760fa603dcae1dcc148172290a8ab15158cf85a953413", size = 182252, upload-time = "2025-03-05T20:02:05.29Z" },
{ url = "https://files.pythonhosted.org/packages/d4/78/2d4fed9123e6620cbf1706c0de8a1632e1a28e7774d94346d7de1bba2ca3/websockets-15.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8fdc51055e6ff4adeb88d58a11042ec9a5eae317a0a53d12c062c8a8865909e8", size = 182521, upload-time = "2025-03-05T20:02:07.458Z" },
{ url = "https://files.pythonhosted.org/packages/e7/3b/66d4c1b444dd1a9823c4a81f50231b921bab54eee2f69e70319b4e21f1ca/websockets-15.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:693f0192126df6c2327cce3baa7c06f2a117575e32ab2308f7f8216c29d9e2e3", size = 181958, upload-time = "2025-03-05T20:02:09.842Z" },
{ url = "https://files.pythonhosted.org/packages/08/ff/e9eed2ee5fed6f76fdd6032ca5cd38c57ca9661430bb3d5fb2872dc8703c/websockets-15.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:54479983bd5fb469c38f2f5c7e3a24f9a4e70594cd68cd1fa6b9340dadaff7cf", size = 181918, upload-time = "2025-03-05T20:02:11.968Z" },
{ url = "https://files.pythonhosted.org/packages/d8/75/994634a49b7e12532be6a42103597b71098fd25900f7437d6055ed39930a/websockets-15.0.1-cp311-cp311-win32.whl", hash = "sha256:16b6c1b3e57799b9d38427dda63edcbe4926352c47cf88588c0be4ace18dac85", size = 176388, upload-time = "2025-03-05T20:02:13.32Z" },
{ url = "https://files.pythonhosted.org/packages/98/93/e36c73f78400a65f5e236cd376713c34182e6663f6889cd45a4a04d8f203/websockets-15.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:27ccee0071a0e75d22cb35849b1db43f2ecd3e161041ac1ee9d2352ddf72f065", size = 176828, upload-time = "2025-03-05T20:02:14.585Z" },
{ url = "https://files.pythonhosted.org/packages/51/6b/4545a0d843594f5d0771e86463606a3988b5a09ca5123136f8a76580dd63/websockets-15.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3", size = 175437, upload-time = "2025-03-05T20:02:16.706Z" },
{ url = "https://files.pythonhosted.org/packages/f4/71/809a0f5f6a06522af902e0f2ea2757f71ead94610010cf570ab5c98e99ed/websockets-15.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665", size = 173096, upload-time = "2025-03-05T20:02:18.832Z" },
{ url = "https://files.pythonhosted.org/packages/3d/69/1a681dd6f02180916f116894181eab8b2e25b31e484c5d0eae637ec01f7c/websockets-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2", size = 173332, upload-time = "2025-03-05T20:02:20.187Z" },
{ url = "https://files.pythonhosted.org/packages/a6/02/0073b3952f5bce97eafbb35757f8d0d54812b6174ed8dd952aa08429bcc3/websockets-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215", size = 183152, upload-time = "2025-03-05T20:02:22.286Z" },
{ url = "https://files.pythonhosted.org/packages/74/45/c205c8480eafd114b428284840da0b1be9ffd0e4f87338dc95dc6ff961a1/websockets-15.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5", size = 182096, upload-time = "2025-03-05T20:02:24.368Z" },
{ url = "https://files.pythonhosted.org/packages/14/8f/aa61f528fba38578ec553c145857a181384c72b98156f858ca5c8e82d9d3/websockets-15.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65", size = 182523, upload-time = "2025-03-05T20:02:25.669Z" },
{ url = "https://files.pythonhosted.org/packages/ec/6d/0267396610add5bc0d0d3e77f546d4cd287200804fe02323797de77dbce9/websockets-15.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe", size = 182790, upload-time = "2025-03-05T20:02:26.99Z" },
{ url = "https://files.pythonhosted.org/packages/02/05/c68c5adbf679cf610ae2f74a9b871ae84564462955d991178f95a1ddb7dd/websockets-15.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4", size = 182165, upload-time = "2025-03-05T20:02:30.291Z" },
{ url = "https://files.pythonhosted.org/packages/29/93/bb672df7b2f5faac89761cb5fa34f5cec45a4026c383a4b5761c6cea5c16/websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597", size = 182160, upload-time = "2025-03-05T20:02:31.634Z" },
{ url = "https://files.pythonhosted.org/packages/ff/83/de1f7709376dc3ca9b7eeb4b9a07b4526b14876b6d372a4dc62312bebee0/websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9", size = 176395, upload-time = "2025-03-05T20:02:33.017Z" },
{ url = "https://files.pythonhosted.org/packages/7d/71/abf2ebc3bbfa40f391ce1428c7168fb20582d0ff57019b69ea20fa698043/websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7", size = 176841, upload-time = "2025-03-05T20:02:34.498Z" },
{ url = "https://files.pythonhosted.org/packages/cb/9f/51f0cf64471a9d2b4d0fc6c534f323b664e7095640c34562f5182e5a7195/websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931", size = 175440, upload-time = "2025-03-05T20:02:36.695Z" },
{ url = "https://files.pythonhosted.org/packages/8a/05/aa116ec9943c718905997412c5989f7ed671bc0188ee2ba89520e8765d7b/websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675", size = 173098, upload-time = "2025-03-05T20:02:37.985Z" },
{ url = "https://files.pythonhosted.org/packages/ff/0b/33cef55ff24f2d92924923c99926dcce78e7bd922d649467f0eda8368923/websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151", size = 173329, upload-time = "2025-03-05T20:02:39.298Z" },