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

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

View File

@@ -1,7 +1,7 @@
name: Safety name: Safety
on: on:
push: push:
branches: [ main ] branches: [ master ]
schedule: schedule:
- cron: '0 0 * * 0' # weekly - cron: '0 0 * * 0' # weekly
jobs: 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 name: Run Tests
on: on:
push: push:
branches: [ main ] branches: [ master ]
pull_request: pull_request:
branches: [ main ] branches: [ master ]
jobs: jobs:
test: test:
@@ -62,12 +60,3 @@ jobs:
- name: Run tests with pytest - name: Run tests with pytest
run: | run: |
poetry run pytest -v --cov=ren_browser --cov-report=xml --cov-report=term 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 - Micron Renderer/Parser
- Android and Flet (config/permissions/etc) - 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 ## Rules
1. Be nice to each other. 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 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 Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal of this software and associated documentation files (the "Software"), to deal

View File

@@ -1,5 +1,5 @@
# Ren Browser Makefile # 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 # Default target
help: help:
@@ -8,12 +8,9 @@ help:
@echo "Available targets:" @echo "Available targets:"
@echo " build - Build the project (alias for poetry-build)" @echo " build - Build the project (alias for poetry-build)"
@echo " poetry-build - Build project with Poetry" @echo " poetry-build - Build project with Poetry"
@echo " run - Launch Ren Browser via Poetry"
@echo " linux - Build Linux package" @echo " linux - Build Linux package"
@echo " apk - Build Android APK" @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 " test - Run tests"
@echo " lint - Run linter" @echo " lint - Run linter"
@echo " format - Format code" @echo " format - Format code"
@@ -36,25 +33,7 @@ linux:
# Android APK build # Android APK build
apk: apk:
@echo "Building Android APK..." @echo "Building Android APK..."
poetry run flet build apk poetry run flet build apk --cleanup-packages --exclude watchdog
# 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
# Development targets # Development targets
test: test:
@@ -69,6 +48,11 @@ format:
@echo "Formatting code..." @echo "Formatting code..."
poetry run ruff format . poetry run ruff format .
# Run application
run:
@echo "Starting Ren Browser..."
poetry run ren-browser
# Clean build artifacts # Clean build artifacts
clean: clean:
@echo "Cleaning build artifacts..." @echo "Cleaning build artifacts..."
@@ -76,5 +60,4 @@ clean:
rm -rf dist/ rm -rf dist/
rm -rf *.egg-info/ rm -rf *.egg-info/
find . -type d -name __pycache__ -exec rm -rf {} + find . -type d -name __pycache__ -exec rm -rf {} +
find . -type f -name "*.pyc" -delete 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/). A browser for the [Reticulum Network](https://reticulum.network/).
> [!WARNING] > [!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/). Built using [Flet](https://flet.dev/).
@@ -21,74 +21,109 @@ Built using [Flet](https://flet.dev/).
- Python 3.13+ - Python 3.13+
- Flet - Flet
- Reticulum 1.0.0+ - Reticulum 1.0.0+
- UV - UV or Poetry
**Setup** **Setup**
Using UV:
```bash ```bash
uv sync uv sync
``` ```
Or using Poetry:
```bash
poetry install
```
### Desktop ### Desktop
Using UV:
```bash ```bash
# From local development # From local development
uv run ren-browser uv run ren-browser
``` ```
Using Poetry:
```bash
poetry run ren-browser
```
### Web ### Web
Using UV:
```bash ```bash
# From local development # From local development
uv run ren-browser-web uv run ren-browser-web
``` ```
Using Poetry:
```bash
poetry run ren-browser-web
```
### Mobile ### Mobile
**Android** **Android**
Using UV:
```bash ```bash
# From local development # From local development
uv run ren-browser-android uv run ren-browser-android
``` ```
Using Poetry:
```bash
poetry run ren-browser-android
```
**iOS** **iOS**
Using UV:
```bash ```bash
# From local development # From local development
uv run ren-browser-ios uv run ren-browser-ios
``` ```
Using Poetry:
```bash
poetry run ren-browser-ios
```
To run directly from the GitHub repository without cloning: To run directly from the GitHub repository without cloning:
```bash ```bash
# Using uvx (temporary environment) # 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 # 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 cd Ren-Browser
uv sync uv sync
uv run ren-browser-web 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 ## Building
### Linux ### Linux
Using UV:
```bash ```bash
uv run flet build linux uv run flet build linux
``` ```
Using Poetry:
```bash
poetry run flet build linux
```
### Android ### Android
Using UV:
```bash ```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 ## 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 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 - [ ] Fix tabs dragging/reordering and overflow issues. https://github.com/Sudo-Ivan/Ren-Browser/issues/1
@@ -41,7 +41,6 @@
## Distribution ## Distribution
- [ ] Add Docker images to build Windows, Linux, MacOS, Android, iOS.
- [ ] Add/Update build workflow to build Windows, MacOS and iOS. - [ ] Add/Update build workflow to build Windows, MacOS and iOS.
- [ ] Appimage - [ ] Appimage
- [ ] Flatpak - [ ] Flatpak

25
poetry.lock generated
View File

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

View File

@@ -7,10 +7,10 @@ authors = [
] ]
module = "ren_browser.app" module = "ren_browser.app"
readme = "README.md" readme = "README.md"
requires-python = ">=3.13" requires-python = ">=3.11"
dependencies = [ dependencies = [
"flet[all] (>=0.28.3,<0.29.0)", "flet (>=0.28.3,<0.29.0)",
"rns (>=1.0.0,<1.5.0)" "rns (>=1.0.2,<1.5.0)"
] ]
[build-system] [build-system]
@@ -35,6 +35,22 @@ dev = [
"pytest-asyncio>=1.2.0,<2.0.0" "pytest-asyncio>=1.2.0,<2.0.0"
] ]
[tool.flet]
exclude = ["watchdog"]
[tool.flet.flutter.pubspec.dependency_overrides] [tool.flet.flutter.pubspec.dependency_overrides]
webview_flutter_android = "4.10.1" 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 argparse
import logging
import os
from pathlib import Path
import flet as ft import flet as ft
import RNS import RNS
from flet import AppView, Page from flet import AppView, Page
from ren_browser import rns
from ren_browser.storage.storage import initialize_storage from ren_browser.storage.storage import initialize_storage
from ren_browser.ui.ui import build_ui from ren_browser.ui.ui import build_ui
RENDERER = "plaintext" RENDERER = "plaintext"
RNS_CONFIG_DIR = None RNS_CONFIG_DIR = None
RNS_INSTANCE = None
logger = logging.getLogger(__name__)
async def main(page: Page): async def main(page: Page):
@@ -49,44 +55,84 @@ async def main(page: Page):
page.add(loader) page.add(loader)
page.update() page.update()
def init_ret(): initialize_storage(page)
import time
time.sleep(0.5) config_override = RNS_CONFIG_DIR
# Initialize storage system print("Initializing Reticulum Network...")
storage = initialize_storage(page) try:
import ren_browser.logs
# Get Reticulum config directory from storage manager ren_browser.logs.setup_rns_logging()
config_dir = storage.get_reticulum_config_path() except Exception:
logger.exception("Unable to configure RNS logging")
# Update the global RNS_CONFIG_DIR so RNS uses the right path success = rns.initialize_reticulum(config_override)
global RNS_CONFIG_DIR if not success:
RNS_CONFIG_DIR = str(config_dir) 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")
# Ensure any saved config is written to filesystem before RNS init page.controls.clear()
try: build_ui(page)
saved_config = storage.load_config() page.update()
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() async def reload_reticulum(page: Page, on_complete=None):
RNS.Reticulum(str(config_dir)) """Hot reload Reticulum with updated configuration.
except (OSError, ValueError):
pass
page.controls.clear()
build_ui(page)
page.update()
page.run_thread(init_ret) 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(): def run():
@@ -101,10 +147,17 @@ def run():
help="Select renderer (plaintext or micron)", help="Select renderer (plaintext or micron)",
) )
parser.add_argument( 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( 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( parser.add_argument(
"-c", "-c",
@@ -120,9 +173,7 @@ def run():
if args.config_dir: if args.config_dir:
RNS_CONFIG_DIR = args.config_dir RNS_CONFIG_DIR = args.config_dir
else: else:
import pathlib RNS_CONFIG_DIR = None
RNS_CONFIG_DIR = str(pathlib.Path.home() / ".reticulum")
if args.web: if args.web:
if args.port is not None: if args.port is not None:

View File

@@ -45,7 +45,7 @@ class PageFetcher:
""" """
RNS.log( 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) dest_bytes = bytes.fromhex(req.destination_hash)
if not RNS.Transport.has_path(dest_bytes): if not RNS.Transport.has_path(dest_bytes):
@@ -87,11 +87,11 @@ class PageFetcher:
req.field_data, req.field_data,
response_callback=on_response, response_callback=on_response,
failed_callback=on_failed, failed_callback=on_failed,
) ),
) )
ev.wait(timeout=15) ev.wait(timeout=15)
data_str = result["data"] or "No content received" data_str = result["data"] or "No content received"
RNS.log( 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 return data_str

View File

@@ -1,27 +1,295 @@
"""Micron markup renderer for Ren Browser. """Micron markup renderer for Ren Browser.
Provides rendering capabilities for micron markup content, Provides rendering capabilities for micron markup content.
currently implemented as a placeholder.
""" """
import re
import flet as ft 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: Args:
content: Micron markup content to render. content: Micron markup content to render.
on_link_click: Optional callback function(url) called when a link is clicked.
Returns: Returns:
ft.Control: Rendered content as a Flet control. ft.Control: Rendered content as a Flet control.
""" """
return ft.Text( try:
content, return _render_micron_internal(content, on_link_click)
selectable=True, except Exception as e:
font_family="monospace", 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, 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 json
import os import os
import pathlib import pathlib
from typing import Any, Dict, Optional from typing import Any
import flet as ft import flet as ft
@@ -19,7 +19,7 @@ class StorageManager:
with platform-specific storage locations. with platform-specific storage locations.
""" """
def __init__(self, page: Optional[ft.Page] = None): def __init__(self, page: ft.Page | None = None):
"""Initialize storage manager. """Initialize storage manager.
Args: Args:
@@ -37,23 +37,23 @@ class StorageManager:
pass pass
if os.name == "posix" and "ANDROID_ROOT" in os.environ: if os.name == "posix" and "ANDROID_ROOT" in os.environ:
# Android - use user-accessible external storage if "ANDROID_DATA" in os.environ:
storage_dir = pathlib.Path("/storage/emulated/0/Documents/ren_browser") storage_dir = pathlib.Path(os.environ["ANDROID_DATA"]) / "ren_browser"
elif hasattr(os, "uname") and "iOS" in str( elif "EXTERNAL_STORAGE" in os.environ:
getattr(os, "uname", lambda: "")() ext_storage = pathlib.Path(os.environ["EXTERNAL_STORAGE"])
).replace("iPhone", "iOS"): storage_dir = ext_storage / "ren_browser"
# iOS - use app's documents directory
storage_dir = pathlib.Path.home() / "Documents" / "ren_browser"
else:
# Desktop (Linux, Windows, macOS) - use home directory
if "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"
)
else: else:
storage_dir = pathlib.Path.home() / ".ren_browser" 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"
else:
storage_dir = pathlib.Path.home() / ".ren_browser"
return storage_dir return storage_dir
@@ -124,7 +124,8 @@ class StorageManager:
if self.page and hasattr(self.page, "client_storage"): 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", config_content)
self.page.client_storage.set( 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 return True
@@ -191,7 +192,8 @@ class StorageManager:
if self.page and hasattr(self.page, "client_storage"): if self.page and hasattr(self.page, "client_storage"):
self.page.client_storage.set( self.page.client_storage.set(
"ren_browser_bookmarks", json.dumps(bookmarks) "ren_browser_bookmarks",
json.dumps(bookmarks),
) )
return True return True
@@ -203,7 +205,7 @@ class StorageManager:
try: try:
bookmarks_path = self._storage_dir / "bookmarks.json" bookmarks_path = self._storage_dir / "bookmarks.json"
if bookmarks_path.exists(): 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) return json.load(f)
if self.page and hasattr(self.page, "client_storage"): if self.page and hasattr(self.page, "client_storage"):
@@ -235,7 +237,7 @@ class StorageManager:
try: try:
history_path = self._storage_dir / "history.json" history_path = self._storage_dir / "history.json"
if history_path.exists(): 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) return json.load(f)
if self.page and hasattr(self.page, "client_storage"): if self.page and hasattr(self.page, "client_storage"):
@@ -248,7 +250,49 @@ class StorageManager:
return [] 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.""" """Get information about the storage system."""
return { return {
"storage_dir": str(self._storage_dir), "storage_dir": str(self._storage_dir),
@@ -272,10 +316,10 @@ class StorageManager:
# Global storage instance # 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.""" """Get the global storage manager instance."""
global _storage_manager global _storage_manager
if _storage_manager is None: if _storage_manager is None:

View File

@@ -8,8 +8,10 @@ from types import SimpleNamespace
import flet as ft 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.micron import render_micron
from ren_browser.renderer.plaintext import render_plaintext from ren_browser.renderer.plaintext import render_plaintext
from ren_browser.storage.storage import get_storage_manager
class TabsManager: class TabsManager:
@@ -30,28 +32,56 @@ class TabsManager:
self.page = page self.page = page
self.page.on_resize = self._on_resize self.page.on_resize = self._on_resize
self.manager = SimpleNamespace(tabs=[], index=0) 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.overflow_menu = None
self.content_container = ft.Container( 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 = ( 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" if app_module.RENDERER == "micron"
else render_plaintext("Welcome to Ren Browser") else render_plaintext("Welcome to Ren Browser")
) )
self._add_tab_internal("Home", default_content) self._add_tab_internal("Home", default_content)
self.add_btn = ft.IconButton( 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( 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.content.controls.append(self.add_btn)
self.tab_bar.controls.append(self.close_btn) self.tab_bar.content.controls.append(self.close_btn)
self.select_tab(0) self.select_tab(0)
self._update_tab_visibility() self._update_tab_visibility()
@@ -59,6 +89,30 @@ class TabsManager:
"""Handle page resize event and update tab visibility.""" """Handle page resize event and update tab visibility."""
self._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: def _update_tab_visibility(self) -> None:
"""Dynamically adjust tab visibility based on page width. """Dynamically adjust tab visibility based on page width.
@@ -67,23 +121,22 @@ class TabsManager:
if not self.page.width or self.page.width == 0: if not self.page.width or self.page.width == 0:
return return
if self.overflow_menu and self.overflow_menu in self.tab_bar.controls: if self.overflow_menu and self.overflow_menu in self.tab_bar.content.controls:
self.tab_bar.controls.remove(self.overflow_menu) self.tab_bar.content.controls.remove(self.overflow_menu)
self.overflow_menu = None self.overflow_menu = None
"""Estimate available width for tabs (Page width - buttons - padding)."""
available_width = self.page.width - 100 available_width = self.page.width - 100
cumulative_width = 0 cumulative_width = 0
visible_tabs_count = 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): 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.content.spacing
estimated_width = len(tab["title"]) * 10 + 32 + self.tab_bar.spacing
"""Always show at least one tab."""
if cumulative_width + estimated_width <= available_width or i == 0: if cumulative_width + estimated_width <= available_width or i == 0:
cumulative_width += estimated_width cumulative_width += estimated_width
if i < len(tab_containers): if i < len(tab_containers):
@@ -93,7 +146,6 @@ class TabsManager:
tab_containers[i].visible = False tab_containers[i].visible = False
if len(self.manager.tabs) > visible_tabs_count: if len(self.manager.tabs) > visible_tabs_count:
"""Move extra tabs to overflow menu."""
overflow_items = [] overflow_items = []
for i in range(visible_tabs_count, len(self.manager.tabs)): for i in range(visible_tabs_count, len(self.manager.tabs)):
tab_data = self.manager.tabs[i] tab_data = self.manager.tabs[i]
@@ -110,7 +162,7 @@ class TabsManager:
items=overflow_items, 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: def _add_tab_internal(self, title: str, content: ft.Control) -> None:
"""Add a new tab to the manager with the given title and content.""" """Add a new tab to the manager with the given title and content."""
@@ -118,17 +170,28 @@ class TabsManager:
url_field = ft.TextField( url_field = ft.TextField(
value=title, value=title,
expand=True, expand=True,
text_style=ft.TextStyle(size=12), text_style=ft.TextStyle(size=14),
content_padding=ft.padding.only(top=8, bottom=8, left=8, right=8), 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( go_btn = ft.IconButton(
ft.Icons.OPEN_IN_BROWSER, ft.Icons.ARROW_FORWARD,
tooltip="Load URL", tooltip="Go",
on_click=lambda e, i=idx: self._on_tab_go(e, i), 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 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( tab_content = ft.Column(
expand=True, expand=True,
scroll=scroll_mode,
controls=[ controls=[
content_control, content_control,
], ],
@@ -143,15 +206,26 @@ class TabsManager:
}, },
) )
tab_container = ft.Container( 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 on_click=lambda e, i=idx: self.select_tab(i), # type: ignore
padding=ft.padding.symmetric(horizontal=12, vertical=6), padding=ft.padding.symmetric(horizontal=16, vertical=10),
border_radius=5, border_radius=8,
bgcolor=ft.Colors.SURFACE_CONTAINER_HIGHEST, 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.content.controls) - 2)
insert_pos = max(0, len(self.tab_bar.controls) - 2) self.tab_bar.content.controls.insert(insert_pos, tab_container)
self.tab_bar.controls.insert(insert_pos, tab_container)
self._update_tab_visibility() self._update_tab_visibility()
def _on_add_click(self, e) -> None: # type: ignore def _on_add_click(self, e) -> None: # type: ignore
@@ -160,8 +234,18 @@ class TabsManager:
content_text = f"Content for {title}" content_text = f"Content for {title}"
import ren_browser.app as app_module 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 = ( content = (
render_micron(content_text) render_micron(content_text, on_link_click=handle_link_click_new)
if app_module.RENDERER == "micron" if app_module.RENDERER == "micron"
else render_plaintext(content_text) else render_plaintext(content_text)
) )
@@ -175,13 +259,17 @@ class TabsManager:
return return
idx = self.manager.index 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] control_to_remove = tab_containers[idx]
self.manager.tabs.pop(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): for i, control in enumerate(updated_tab_containers):
control.on_click = lambda e, i=i: self.select_tab(i) # type: ignore control.on_click = lambda e, i=i: self.select_tab(i) # type: ignore
@@ -199,12 +287,16 @@ class TabsManager:
""" """
self.manager.index = idx 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): for i, control in enumerate(tab_containers):
if i == idx: 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: 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.content_container.content = self.manager.tabs[idx]["content"]
self.page.update() self.page.update()
@@ -215,16 +307,68 @@ class TabsManager:
url = tab["url_field"].value.strip() url = tab["url_field"].value.strip()
if not url: if not url:
return return
placeholder_text = f"Loading content for {url}"
placeholder_text = f"Loading content for {url}..."
import ren_browser.app as app_module import ren_browser.app as app_module
new_control = ( current_node_hash = None
render_micron(placeholder_text) 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" if app_module.RENDERER == "micron"
else render_plaintext(placeholder_text) else render_plaintext(placeholder_text)
) )
tab["content_control"] = new_control tab["content_control"] = placeholder_control
tab["content"].controls[0] = new_control tab["content"].controls[0] = placeholder_control
if self.manager.index == idx: if self.manager.index == idx:
self.content_container.content = tab["content"] self.content_container.content = tab["content"]
self.page.update() 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 from __future__ import annotations
information display.
""" from datetime import datetime
from pathlib import Path
import logging
import flet as ft 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 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): def open_settings_tab(page: ft.Page, tab_manager):
"""Open a settings tab with configuration and debugging options. """Open a settings tab with configuration, status, and storage info."""
Args:
page: Flet page instance for UI updates.
tab_manager: Tab manager to add the settings tab to.
"""
storage = get_storage_manager(page) storage = get_storage_manager(page)
config_path = _get_config_file_path()
try: config_text = _read_config_text(config_path)
config_text = storage.load_config() app_settings = storage.load_app_settings()
except Exception as ex:
config_text = f"Error reading config: {ex}"
config_field = ft.TextField( config_field = ft.TextField(
label="Reticulum config", label="Reticulum Configuration",
value=config_text, value=config_text,
expand=True, expand=True,
multiline=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: try:
success = storage.save_config(config_field.value) color_preview.bgcolor = page_bgcolor_field.value
if success: page.update()
print("Config saved successfully. Please restart the app.") except Exception as exc:
page.snack_bar = ft.SnackBar( logger.warning(
ft.Text("Config saved successfully. Please restart the app."), "Ignoring invalid background color '%s': %s",
open=True, page_bgcolor_field.value,
) exc,
else:
print("Error saving config: Storage operation failed")
page.snack_bar = ft.SnackBar(
ft.Text("Error saving config: Storage operation failed"), open=True
)
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_btn = ft.ElevatedButton("Save Config", on_click=on_save_config) page_bgcolor_field.on_change = on_bgcolor_change
error_field = ft.TextField(
label="Error Logs", def show_snack(message, *, success=True):
value="", snack = ft.SnackBar(
expand=True, content=ft.Row(
multiline=True, controls=[
read_only=True, 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:
show_snack("Reticulum reloaded successfully!")
else:
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(),
) )
ret_field = ft.TextField( save_reload_btn = ft.ElevatedButton(
label="Reticulum logs", "Save & Hot Reload",
value="", icon=ft.Icons.REFRESH,
expand=True, on_click=on_save_and_reload_config,
multiline=True, style=_blue_button_style(),
read_only=True, )
save_appearance_btn = ft.ElevatedButton(
"Save Appearance",
icon=ft.Icons.PALETTE,
on_click=on_save_app_settings,
style=_blue_button_style(),
) )
# Storage information for debugging status_content, refresh_status_section = _build_status_section(page)
storage_info = storage.get_storage_info() storage_field, refresh_storage_info = _build_storage_field(storage)
storage_text = "\n".join([f"{key}: {value}" for key, value in storage_info.items()])
storage_field = ft.TextField( appearance_content = ft.Column(
label="Storage Information", spacing=16,
value=storage_text, controls=[
expand=True, ft.Text("Appearance Settings", size=18, weight=ft.FontWeight.BOLD),
multiline=True, horizontal_scroll_switch,
read_only=True, ft.Row(
controls=[page_bgcolor_field, color_preview],
alignment=ft.MainAxisAlignment.START,
spacing=16,
),
save_appearance_btn,
],
) )
content_placeholder = ft.Container(expand=True) content_placeholder = ft.Container(expand=True, content=config_field)
def show_config(ev): def show_config(_):
content_placeholder.content = config_field content_placeholder.content = config_field
page.update() page.update()
def show_errors(ev): def show_appearance(_):
error_field.value = "\n".join(ERROR_LOGS) or "No errors logged." content_placeholder.content = appearance_content
content_placeholder.content = error_field
page.update() page.update()
def show_ret_logs(ev): def show_status(_):
ret_field.value = "\n".join(RET_LOGS) or "No Reticulum logs." content_placeholder.content = status_content
content_placeholder.content = ret_field refresh_status_section()
page.update()
def show_storage_info(ev): def show_storage_info(_):
storage_info = storage.get_storage_info() refresh_storage_info()
storage_field.value = "\n".join(
[f"{key}: {value}" for key, value in storage_info.items()]
)
content_placeholder.content = storage_field content_placeholder.content = storage_field
page.update() page.update()
def refresh_current_view(ev): def refresh_current_view(_):
# Refresh the currently displayed content if content_placeholder.content == status_content:
if content_placeholder.content == error_field: refresh_status_section()
show_errors(ev)
elif content_placeholder.content == ret_field:
show_ret_logs(ev)
elif content_placeholder.content == storage_field: elif content_placeholder.content == storage_field:
show_storage_info(ev) refresh_storage_info()
elif content_placeholder.content == config_field: page.update()
show_config(ev)
btn_config = ft.ElevatedButton("Config", on_click=show_config) btn_config = ft.FilledButton(
btn_errors = ft.ElevatedButton("Errors", on_click=show_errors) "Configuration",
btn_ret = ft.ElevatedButton("Ret Logs", on_click=show_ret_logs) icon=ft.Icons.SETTINGS,
btn_storage = ft.ElevatedButton("Storage", on_click=show_storage_info) on_click=show_config,
btn_refresh = ft.ElevatedButton("Refresh", on_click=refresh_current_view) style=_blue_button_style(),
button_row = ft.Row(
controls=[btn_config, btn_errors, btn_ret, btn_storage, btn_refresh]
) )
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( settings_content = ft.Column(
expand=True, expand=True,
spacing=16,
controls=[ controls=[
button_row, ft.Container(
content_placeholder, content=ft.Text(
ft.Row([save_btn]), "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) tab_manager._add_tab_internal("Settings", settings_content)
idx = len(tab_manager.manager.tabs) - 1 idx = len(tab_manager.manager.tabs) - 1
tab_manager.select_tab(idx) tab_manager.select_tab(idx)

View File

@@ -23,11 +23,26 @@ def build_ui(page: Page):
""" """
page.theme_mode = ft.ThemeMode.DARK 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.window.maximized = True
page.padding = 0
page_fetcher = PageFetcher() 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): def update_announces(ann_list):
announce_list.controls.clear() announce_list.controls.clear()
@@ -58,8 +73,21 @@ def build_ui(page: Page):
tab = tab_manager.manager.tabs[idx] tab = tab_manager.manager.tabs[idx]
except IndexError: except IndexError:
return 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"): if req.page_path.endswith(".mu"):
new_control = render_micron(result) new_control = render_micron(
result,
on_link_click=handle_link_click,
)
else: else:
new_control = render_plaintext(result) new_control = render_plaintext(result)
tab["content_control"] = new_control tab["content_control"] = new_control
@@ -70,25 +98,50 @@ def build_ui(page: Page):
page.run_thread(fetch_and_update) 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() page.update()
AnnounceService(update_callback=update_announces) AnnounceService(update_callback=update_announces)
page.drawer = ft.NavigationDrawer( page.drawer = ft.NavigationDrawer(
bgcolor=ft.Colors.GREY_900,
elevation=8,
controls=[ controls=[
ft.Text( ft.Container(
"Announcements", content=ft.Text(
weight=ft.FontWeight.BOLD, "Announcements",
text_align=ft.TextAlign.CENTER, size=20,
expand=True, weight=ft.FontWeight.BOLD,
color=ft.Colors.BLUE_400,
),
padding=ft.padding.symmetric(horizontal=16, vertical=20),
), ),
ft.Divider(), ft.Divider(height=1, color=ft.Colors.GREY_700),
announce_list, announce_list,
], ],
) )
page.appbar.leading = ft.IconButton( page.appbar.leading = ft.IconButton(
ft.Icons.MENU, ft.Icons.MENU,
tooltip="Toggle sidebar", tooltip="Announcements",
icon_color=ft.Colors.WHITE,
on_click=lambda e: ( on_click=lambda e: (
setattr(page.drawer, "open", not page.drawer.open), setattr(page.drawer, "open", not page.drawer.open),
page.update(), page.update(),
@@ -102,15 +155,21 @@ def build_ui(page: Page):
ft.IconButton( ft.IconButton(
ft.Icons.SETTINGS, ft.Icons.SETTINGS,
tooltip="Settings", tooltip="Settings",
icon_color=ft.Colors.WHITE,
on_click=lambda e: open_settings_tab(page, tab_manager), on_click=lambda e: open_settings_tab(page, tab_manager),
) ),
] ]
Shortcuts(page, tab_manager) Shortcuts(page, tab_manager)
url_bar = ft.Row( url_bar = ft.Container(
controls=[ content=ft.Row(
tab_manager.manager.tabs[tab_manager.manager.index]["url_field"], controls=[
tab_manager.manager.tabs[tab_manager.manager.index]["go_btn"], 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 page.appbar.title = url_bar
orig_select_tab = tab_manager.select_tab orig_select_tab = tab_manager.select_tab
@@ -118,8 +177,8 @@ def build_ui(page: Page):
def _select_tab_and_update_url(i): def _select_tab_and_update_url(i):
orig_select_tab(i) orig_select_tab(i)
tab = tab_manager.manager.tabs[i] tab = tab_manager.manager.tabs[i]
url_bar.controls.clear() url_bar.content.controls.clear()
url_bar.controls.extend([tab["url_field"], tab["go_btn"]]) url_bar.content.controls.extend([tab["url_field"], tab["go_btn"]])
page.update() page.update()
tab_manager.select_tab = _select_tab_and_update_url 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 from ren_browser.pages.page_request import PageRequest
return 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.save_config.return_value = True
mock_storage.get_config_path.return_value = Mock() mock_storage.get_config_path.return_value = Mock()
mock_storage.get_reticulum_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 = { mock_storage.get_storage_info.return_value = {
"storage_dir": "/mock/storage", "storage_dir": "/mock/storage",
"config_path": "/mock/storage/config.txt", "config_path": "/mock/storage/config.txt",

View File

@@ -1,5 +1,6 @@
from unittest.mock import Mock from unittest.mock import Mock
import flet as ft
import pytest import pytest
from ren_browser import app from ren_browser import app
@@ -14,16 +15,21 @@ class TestAppIntegration:
mock_page = Mock() mock_page = Mock()
mock_page.add = Mock() mock_page.add = Mock()
mock_page.update = Mock() mock_page.update = Mock()
mock_page.run_thread = Mock()
mock_page.controls = Mock() mock_page.controls = Mock()
mock_page.controls.clear = 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) await app.main(mock_page)
# Verify that the main function sets up the loading screen assert mock_page.add.call_count >= 1
mock_page.add.assert_called_once() loader_call = mock_page.add.call_args_list[0][0][0]
assert isinstance(loader_call, ft.Container)
mock_page.update.assert_called() mock_page.update.assert_called()
mock_page.run_thread.assert_called_once()
def test_entry_points_exist(self): def test_entry_points_exist(self):
"""Test that all expected entry points exist and are callable.""" """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): def test_announce_with_none_display_name(self):
"""Test Announce creation with None display name.""" """Test Announce creation with None display name."""
announce = Announce( announce = Announce(
destination_hash="1234567890abcdef", display_name=None, timestamp=1234567890 destination_hash="1234567890abcdef",
display_name=None,
timestamp=1234567890,
) )
assert announce.destination_hash == "1234567890abcdef" assert announce.destination_hash == "1234567890abcdef"

View File

@@ -12,26 +12,34 @@ class TestApp:
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_main_initializes_loader(self, mock_page, mock_rns): async def test_main_initializes_loader(self, mock_page, mock_rns):
"""Test that main function initializes with loading screen.""" """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) 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.update.assert_called()
mock_page.run_thread.assert_called_once()
@pytest.mark.asyncio @pytest.mark.asyncio
async def test_main_function_structure(self, mock_page, mock_rns): async def test_main_function_structure(self, mock_page, mock_rns):
"""Test that main function sets up the expected structure.""" """Test that main function sets up the expected structure."""
await app.main(mock_page) 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 assert mock_page.add.call_count >= 1
mock_page.add.assert_called_once() loader_call = mock_page.add.call_args_list[0][0][0]
assert isinstance(loader_call, ft.Container)
mock_page.update.assert_called() 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): def test_run_with_default_args(self, mock_rns):
"""Test run function with default arguments.""" """Test run function with default arguments."""

View File

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

View File

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

View File

@@ -63,66 +63,58 @@ class TestMicronRenderer:
""" """
def test_render_micron_basic(self): def test_render_micron_basic(self):
"""Test basic micron rendering (currently displays raw content).""" """Test basic micron rendering."""
content = "# Heading\n\nSome content" content = "# Heading\n\nSome content"
result = render_micron(content) result = render_micron(content)
assert isinstance(result, ft.Text) assert isinstance(result, ft.Column)
assert result.value == "# Heading\n\nSome content"
assert result.selectable is True
assert result.font_family == "monospace"
assert result.expand is True assert result.expand is True
assert result.scroll == ft.ScrollMode.AUTO
def test_render_micron_empty(self): def test_render_micron_empty(self):
"""Test micron rendering with empty content.""" """Test micron rendering with empty content."""
content = "" content = ""
result = render_micron(content) result = render_micron(content)
assert isinstance(result, ft.Text) assert isinstance(result, ft.Column)
assert result.value == "" assert len(result.controls) >= 0
assert result.selectable is True
def test_render_micron_unicode(self): def test_render_micron_unicode(self):
"""Test micron rendering with Unicode characters.""" """Test micron rendering with Unicode characters."""
content = "Unicode content: 你好 🌍 αβγ" content = "Unicode content: 你好 🌍 αβγ"
result = render_micron(content) result = render_micron(content)
assert isinstance(result, ft.Text) assert isinstance(result, ft.Column)
assert result.value == content assert len(result.controls) > 0
assert result.selectable is True
class TestRendererComparison: class TestRendererComparison:
"""Test cases comparing both renderers.""" """Test cases comparing both renderers."""
def test_renderers_return_same_type(self): 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" content = "Test content"
plaintext_result = render_plaintext(content) plaintext_result = render_plaintext(content)
micron_result = render_micron(content) micron_result = render_micron(content)
assert type(plaintext_result) is type(micron_result)
assert isinstance(plaintext_result, ft.Text) assert isinstance(plaintext_result, ft.Text)
assert isinstance(micron_result, ft.Text) assert isinstance(micron_result, ft.Column)
def test_renderers_preserve_content(self): 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" content = "Test content with\nmultiple lines"
plaintext_result = render_plaintext(content) plaintext_result = render_plaintext(content)
micron_result = render_micron(content)
assert plaintext_result.value == content assert plaintext_result.value == content
assert micron_result.value == content
def test_renderers_same_properties(self): 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" content = "Test content"
plaintext_result = render_plaintext(content) plaintext_result = render_plaintext(content)
micron_result = render_micron(content) micron_result = render_micron(content)
assert plaintext_result.selectable == micron_result.selectable assert plaintext_result.expand is True
assert plaintext_result.font_family == micron_result.font_family assert micron_result.expand is True
assert plaintext_result.expand == micron_result.expand

View File

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

View File

@@ -34,8 +34,8 @@ class TestTabsManager:
assert isinstance(manager.manager, SimpleNamespace) assert isinstance(manager.manager, SimpleNamespace)
assert len(manager.manager.tabs) == 1 assert len(manager.manager.tabs) == 1
assert manager.manager.index == 0 assert manager.manager.index == 0
assert isinstance(manager.tab_bar, ft.Row) assert isinstance(manager.tab_bar, ft.Container)
assert manager.tab_bar.scroll is None assert isinstance(manager.tab_bar.content, ft.Row)
assert manager.overflow_menu is None assert manager.overflow_menu is None
assert isinstance(manager.content_container, ft.Container) assert isinstance(manager.content_container, ft.Container)
@@ -105,12 +105,14 @@ class TestTabsManager:
"""Test that selecting a tab updates background colors correctly.""" """Test that selecting a tab updates background colors correctly."""
tabs_manager._add_tab_internal("Tab 2", Mock()) 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) tabs_manager.select_tab(1)
assert tab_controls[0].bgcolor == ft.Colors.SURFACE_CONTAINER_HIGHEST assert tab_controls[0].bgcolor == ft.Colors.GREY_800
assert tab_controls[1].bgcolor == ft.Colors.PRIMARY_CONTAINER assert tab_controls[1].bgcolor == ft.Colors.BLUE_900
def test_on_tab_go_empty_url(self, tabs_manager): def test_on_tab_go_empty_url(self, tabs_manager):
"""Test tab go with empty URL.""" """Test tab go with empty URL."""
@@ -146,12 +148,12 @@ class TestTabsManager:
def test_tab_container_properties(self, tabs_manager): def test_tab_container_properties(self, tabs_manager):
"""Test that tab container has correct properties.""" """Test that tab container has correct properties."""
assert tabs_manager.content_container.expand is True assert tabs_manager.content_container.expand is True
assert tabs_manager.content_container.bgcolor == ft.Colors.BLACK assert tabs_manager.content_container.bgcolor in (ft.Colors.BLACK, "#000000")
assert tabs_manager.content_container.padding == ft.padding.all(5) assert tabs_manager.content_container.padding == ft.padding.all(16)
def test_tab_bar_controls(self, tabs_manager): def test_tab_bar_controls(self, tabs_manager):
"""Test that tab bar has correct controls.""" """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) # Should have: home tab, add button, close button (and potentially overflow menu)
assert len(controls) >= 3 assert len(controls) >= 3
@@ -180,7 +182,7 @@ class TestTabsManager:
url_field = tab["url_field"] url_field = tab["url_field"]
assert url_field.expand is True 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 assert url_field.content_padding is not None
def test_go_button_properties(self, tabs_manager): def test_go_button_properties(self, tabs_manager):
@@ -188,14 +190,16 @@ class TestTabsManager:
tab = tabs_manager.manager.tabs[0] tab = tabs_manager.manager.tabs[0]
go_btn = tab["go_btn"] go_btn = tab["go_btn"]
assert go_btn.icon == ft.Icons.OPEN_IN_BROWSER assert go_btn.icon == ft.Icons.ARROW_FORWARD
assert go_btn.tooltip == "Load URL" assert go_btn.tooltip == "Go"
def test_tab_click_handlers(self, tabs_manager): def test_tab_click_handlers(self, tabs_manager):
"""Test that tab click handlers are properly set.""" """Test that tab click handlers are properly set."""
tabs_manager._add_tab_internal("Tab 2", Mock()) 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): for i, control in enumerate(tab_controls):
assert control.on_click is not None assert control.on_click is not None
@@ -240,22 +244,30 @@ class TestTabsManager:
def test_adaptive_overflow_behavior(self, tabs_manager): def test_adaptive_overflow_behavior(self, tabs_manager):
"""Test that the overflow menu adapts to tab changes.""" """Test that the overflow menu adapts to tab changes."""
# With page width at 800, add enough tabs that some should overflow. # With page width at 800, add enough tabs that some should overflow.
for i in range(10): # Total 11 tabs for i in range(10): # Total 11 tabs
tabs_manager._add_tab_internal(f"Tab {i + 2}", Mock()) tabs_manager._add_tab_internal(f"Tab {i + 2}", Mock())
# Check that an overflow menu exists # Check that an overflow menu exists
assert tabs_manager.overflow_menu is not None assert tabs_manager.overflow_menu is not None
# Simulate a smaller screen, expecting more tabs to overflow # Simulate a smaller screen, expecting more tabs to overflow
tabs_manager.page.width = 400 tabs_manager.page.width = 400
tabs_manager._update_tab_visibility() 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 assert visible_tabs_small < 11
# Simulate a larger screen, expecting all tabs to be visible # Simulate a larger screen, expecting all tabs to be visible
tabs_manager.page.width = 1600 tabs_manager.page.width = 1600
tabs_manager._update_tab_visibility() 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 visible_tabs_large == 11
assert tabs_manager.overflow_menu is None assert tabs_manager.overflow_menu is None

View File

@@ -29,7 +29,12 @@ class TestBuildUI:
@patch("ren_browser.tabs.tabs.TabsManager") @patch("ren_browser.tabs.tabs.TabsManager")
@patch("ren_browser.controls.shortcuts.Shortcuts") @patch("ren_browser.controls.shortcuts.Shortcuts")
def test_build_ui_appbar_setup( 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.""" """Test that build_ui sets up the app bar correctly."""
mock_tab_manager = Mock() mock_tab_manager = Mock()
@@ -51,7 +56,12 @@ class TestBuildUI:
@patch("ren_browser.tabs.tabs.TabsManager") @patch("ren_browser.tabs.tabs.TabsManager")
@patch("ren_browser.controls.shortcuts.Shortcuts") @patch("ren_browser.controls.shortcuts.Shortcuts")
def test_build_ui_drawer_setup( 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.""" """Test that build_ui sets up the drawer correctly."""
mock_tab_manager = Mock() mock_tab_manager = Mock()
@@ -83,28 +93,50 @@ class TestBuildUI:
class TestOpenSettingsTab: class TestOpenSettingsTab:
"""Test cases for the open_settings_tab function.""" """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.""" """Test opening settings tab with basic functionality."""
mock_tab_manager = Mock() mock_tab_manager = Mock()
mock_tab_manager.manager.tabs = [] mock_tab_manager.manager.tabs = []
mock_tab_manager._add_tab_internal = Mock() mock_tab_manager._add_tab_internal = Mock()
mock_tab_manager.select_tab = 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) open_settings_tab(mock_page, mock_tab_manager)
mock_tab_manager._add_tab_internal.assert_called_once() mock_tab_manager._add_tab_internal.assert_called_once()
mock_tab_manager.select_tab.assert_called_once() mock_tab_manager.select_tab.assert_called_once()
mock_page.update.assert_called() 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.""" """Test opening settings tab when config file cannot be read."""
mock_tab_manager = Mock() mock_tab_manager = Mock()
mock_tab_manager.manager.tabs = [] mock_tab_manager.manager.tabs = []
mock_tab_manager._add_tab_internal = Mock() mock_tab_manager._add_tab_internal = Mock()
mock_tab_manager.select_tab = 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) open_settings_tab(mock_page, mock_tab_manager)
mock_tab_manager._add_tab_internal.assert_called_once() mock_tab_manager._add_tab_internal.assert_called_once()
@@ -113,69 +145,110 @@ class TestOpenSettingsTab:
args = mock_tab_manager._add_tab_internal.call_args args = mock_tab_manager._add_tab_internal.call_args
assert args[0][0] == "Settings" 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.""" """Test saving config successfully in settings."""
mock_tab_manager = Mock() mock_tab_manager = Mock()
mock_tab_manager.manager.tabs = [] mock_tab_manager.manager.tabs = []
mock_tab_manager._add_tab_internal = Mock() mock_tab_manager._add_tab_internal = Mock()
mock_tab_manager.select_tab = Mock() mock_tab_manager.select_tab = Mock()
with ( mock_page.overlay = []
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()
with ( with (
patch( patch(
"ren_browser.ui.settings.get_storage_manager", "ren_browser.ui.settings.get_storage_manager",
return_value=mock_storage_manager, return_value=mock_storage_manager,
), ),
patch("ren_browser.logs.ERROR_LOGS", ["Error 1", "Error 2"]), patch(
patch("ren_browser.logs.RET_LOGS", ["RNS log 1", "RNS log 2"]), "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) open_settings_tab(mock_page, mock_tab_manager)
mock_tab_manager._add_tab_internal.assert_called_once() # Get the settings content that was added
args = mock_tab_manager._add_tab_internal.call_args settings_content = mock_tab_manager._add_tab_internal.call_args[0][1]
assert args[0][0] == "Settings"
# 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 version = 1
revision = 3 revision = 3
requires-python = ">=3.13" requires-python = ">=3.11"
[[package]] [[package]]
name = "annotated-doc" name = "annotated-doc"
@@ -27,6 +27,7 @@ source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "idna" }, { name = "idna" },
{ name = "sniffio" }, { 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" } 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 = [ 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" } 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 = [ 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/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/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" },
{ url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, { url = "https://files.pythonhosted.org/packages/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" } 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" } 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 = [ 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/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/7d/62/73a6d7450829655a35bb88a88fca7d736f9882a27eacdca2c6d505b57e2e/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed", size = 147936, upload-time = "2025-10-14T04:41:14.461Z" },
{ url = "https://files.pythonhosted.org/packages/89/c5/adb8c8b3d6625bef6d88b251bbb0d95f8205831b987631ab0c8bb5d937c2/charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72", size = 144180, upload-time = "2025-10-14T04:41:15.588Z" }, { url = "https://files.pythonhosted.org/packages/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" } 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" } 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 = [ 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/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/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" }, { 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" }, { 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]] [[package]]
name = "cryptography" name = "cryptography"
version = "46.0.3" 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/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/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/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]] [[package]]
@@ -441,6 +536,20 @@ version = "0.7.1"
source = { registry = "https://pypi.org/simple" } 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" } 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 = [ 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/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/0a/1a/22887f53602feaa066354867bc49a68fc295c2293433177ee90870a7d517/httptools-0.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:601b7628de7504077dd3dcb3791c6b8694bbd967148a6d1f01806509254fb1ca", size = 108180, upload-time = "2025-10-10T03:54:48.052Z" },
{ url = "https://files.pythonhosted.org/packages/32/6a/6aaa91937f0010d288d3d124ca2946d48d60c3a5ee7ca62afe870e3ea011/httptools-0.7.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:04c6c0e6c5fb0739c5b8a9eb046d298650a0ff38cf42537fc372b28dc7e4472c", size = 478596, upload-time = "2025-10-10T03:54:48.919Z" }, { url = "https://files.pythonhosted.org/packages/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" } 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" } 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 = [ 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/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/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" },
{ url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, { url = "https://files.pythonhosted.org/packages/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" } 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 = [ 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/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/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" }, { 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/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/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/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]] [[package]]
@@ -724,6 +899,7 @@ version = "1.2.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "pytest" }, { 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" } 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 = [ wheels = [
@@ -735,7 +911,7 @@ name = "pytest-cov"
version = "7.0.0" version = "7.0.0"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "coverage" }, { name = "coverage", extra = ["toml"] },
{ name = "pluggy" }, { name = "pluggy" },
{ name = "pytest" }, { name = "pytest" },
] ]
@@ -795,6 +971,25 @@ version = "6.0.3"
source = { registry = "https://pypi.org/simple" } 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" } 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 = [ 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/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/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" },
{ url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, { url = "https://files.pythonhosted.org/packages/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] [package.metadata]
requires-dist = [ requires-dist = [
{ name = "flet", extras = ["all"], specifier = ">=0.28.3,<0.29.0" }, { 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] [package.metadata.requires-dev]
@@ -914,15 +1109,15 @@ wheels = [
[[package]] [[package]]
name = "rns" name = "rns"
version = "1.0.1" version = "1.0.2"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "cryptography" }, { name = "cryptography" },
{ name = "pyserial" }, { 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 = [ 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]] [[package]]
@@ -975,6 +1170,7 @@ version = "0.49.3"
source = { registry = "https://pypi.org/simple" } source = { registry = "https://pypi.org/simple" }
dependencies = [ dependencies = [
{ name = "anyio" }, { 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" } 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 = [ 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" }, { 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]] [[package]]
name = "typing-extensions" name = "typing-extensions"
version = "4.15.0" version = "4.15.0"
@@ -1068,6 +1313,18 @@ version = "0.22.1"
source = { registry = "https://pypi.org/simple" } 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" } 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 = [ 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/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/d2/14/e301ee96a6dc95224b6f1162cd3312f6d1217be3907b79173b06785f2fe7/uvloop-0.22.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cdf5192ab3e674ca26da2eada35b288d2fa49fdd0f357a19f0e7c4e7d5077c8", size = 751811, upload-time = "2025-10-16T22:16:38.275Z" },
{ url = "https://files.pythonhosted.org/packages/b7/02/654426ce265ac19e2980bfd9ea6590ca96a56f10c76e63801a2df01c0486/uvloop-0.22.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e2ea3d6190a2968f4a14a23019d3b16870dd2190cd69c8180f7c632d21de68d", size = 4288562, upload-time = "2025-10-16T22:16:39.375Z" }, { url = "https://files.pythonhosted.org/packages/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" } 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" } 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 = [ 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/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/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" }, { 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" } 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 = [ 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/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/2b/f9/f07a295cde762644aa4c4bb0f88921d2d141af45e735b965fb2e87858328/watchfiles-1.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5f3bde70f157f84ece3765b42b4a52c6ac1a50334903c6eaf765362f6ccca88a", size = 391783, upload-time = "2025-10-14T15:05:03.052Z" },
{ url = "https://files.pythonhosted.org/packages/bc/11/fc2502457e0bea39a5c958d86d2cb69e407a4d00b85735ca724bfa6e0d1a/watchfiles-1.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:14e0b1fe858430fc0251737ef3824c54027bedb8c37c38114488b8e131cf8219", size = 449279, upload-time = "2025-10-14T15:05:04.004Z" }, { url = "https://files.pythonhosted.org/packages/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/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/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/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]] [[package]]
@@ -1172,6 +1465,28 @@ version = "15.0.1"
source = { registry = "https://pypi.org/simple" } 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" } 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 = [ 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/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/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" }, { 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" },