Compare commits
36 Commits
support-uv
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
859c15653c
|
|||
|
b063c0cbec
|
|||
|
3bb64d0533
|
|||
|
64fa46f60f
|
|||
|
e05a94e152
|
|||
|
b5ad57ad8f
|
|||
|
3c149cdd0c
|
|||
|
a1480a5c1b
|
|||
|
1e39fe277e
|
|||
|
d8de2b1150
|
|||
|
d1536aa05a
|
|||
|
1882325224
|
|||
|
cce6471534
|
|||
|
8bd46f50f3
|
|||
|
1e9b53934f
|
|||
|
c2921876f7
|
|||
|
8d723a8944
|
|||
|
f3d0da08a2
|
|||
|
5571b810ae
|
|||
|
df72547bde
|
|||
|
5ec677437e
|
|||
|
3cddaeb2b9
|
|||
|
e36bfec4a0
|
|||
|
0aaa7938e6
|
|||
|
379f85c792
|
|||
|
bfbfb22312
|
|||
|
5f9d7784a8
|
|||
|
9c0564d253
|
|||
|
5da3be18cb
|
|||
|
9eb9aafd35
|
|||
| 263e5a92bf | |||
| 6b5d476d74 | |||
| 5d640032ee | |||
| 0d2d595867 | |||
| a32a542c54 | |||
| e77faa5105 |
@@ -9,6 +9,3 @@ name = "python"
|
|||||||
|
|
||||||
[analyzers.meta]
|
[analyzers.meta]
|
||||||
runtime_version = "3.x.x"
|
runtime_version = "3.x.x"
|
||||||
|
|
||||||
[[analyzers]]
|
|
||||||
name = "docker"
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
config/
|
|
||||||
__pycache__/
|
|
||||||
build/
|
|
||||||
dist/
|
|
||||||
.ruff_cache/
|
|
||||||
.env
|
|
||||||
To-Do.md
|
|
||||||
@@ -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
|
||||||
@@ -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:
|
||||||
@@ -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
|
|
||||||
55
.github/workflows/docker.yml
vendored
55
.github/workflows/docker.yml
vendored
@@ -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
|
|
||||||
@@ -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.
|
||||||
|
|||||||
29
Dockerfile
29
Dockerfile
@@ -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"]
|
|
||||||
2
LICENSE
2
LICENSE
@@ -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
|
||||||
|
|||||||
33
Makefile
33
Makefile
@@ -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..."
|
||||||
@@ -77,4 +61,3 @@ clean:
|
|||||||
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
|
|
||||||
|
|||||||
68
README.md
68
README.md
@@ -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,73 +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
|
||||||
|
```
|
||||||
|
|
||||||
# Or run directly from GitHub
|
Using Poetry:
|
||||||
uvx git+https://github.com/Sudo-Ivan/Ren-Browser.git -- ren-browser
|
```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
|
||||||
|
```
|
||||||
|
|
||||||
# Or run directly from GitHub
|
Using Poetry:
|
||||||
uvx git+https://github.com/Sudo-Ivan/Ren-Browser.git -- ren-browser-web
|
```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
|
||||||
|
```
|
||||||
|
|
||||||
# Or run directly from GitHub
|
Using Poetry:
|
||||||
uvx git+https://github.com/Sudo-Ivan/Ren-Browser.git -- ren-browser-android
|
```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
|
||||||
|
|
||||||
# Or run directly from GitHub
|
|
||||||
uvx git+https://github.com/Sudo-Ivan/Ren-Browser.git -- ren-browser-ios
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Docker/Podman
|
Using Poetry:
|
||||||
|
```bash
|
||||||
|
poetry run ren-browser-ios
|
||||||
|
```
|
||||||
|
|
||||||
|
To run directly from the GitHub repository without cloning:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker build -t ren-browser .
|
# Using uvx (temporary environment)
|
||||||
docker run -p 8550:8550 -v ./config:/app/config ren-browser
|
uvx --from git+https://git.quad4.io/Ren/Browser.git ren-browser-web
|
||||||
|
|
||||||
|
# Or clone and run locally
|
||||||
|
git clone https://git.quad4.io/Ren/Browser.git
|
||||||
|
cd Ren-Browser
|
||||||
|
uv sync
|
||||||
|
uv run ren-browser-web
|
||||||
```
|
```
|
||||||
|
|
||||||
## 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
|
||||||
```
|
```
|
||||||
3
To-Do.md
3
To-Do.md
@@ -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
25
poetry.lock
generated
@@ -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"
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
289
ren_browser/rns.py
Normal 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()
|
||||||
@@ -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:
|
||||||
|
|||||||
@@ -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:
|
||||||
@@ -18,7 +20,7 @@ class TabsManager:
|
|||||||
Handles tab creation, switching, closing, and content rendering.
|
Handles tab creation, switching, closing, and content rendering.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, page: ft.Page):
|
def __init__(self, page: ft.Page) -> None:
|
||||||
"""Initialize the tab manager.
|
"""Initialize the tab manager.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -28,43 +30,168 @@ class TabsManager:
|
|||||||
import ren_browser.app as app_module
|
import ren_browser.app as app_module
|
||||||
|
|
||||||
self.page = page
|
self.page = page
|
||||||
|
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.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.extend([self.add_btn, self.close_btn])
|
self.tab_bar.content.controls.append(self.add_btn)
|
||||||
|
self.tab_bar.content.controls.append(self.close_btn)
|
||||||
self.select_tab(0)
|
self.select_tab(0)
|
||||||
|
self._update_tab_visibility()
|
||||||
|
|
||||||
def _add_tab_internal(self, title: str, content: ft.Control):
|
def _on_resize(self, e) -> None: # type: ignore
|
||||||
|
"""Handle page resize event and update tab visibility."""
|
||||||
|
self._update_tab_visibility()
|
||||||
|
|
||||||
|
def apply_settings(self, settings: dict) -> None:
|
||||||
|
"""Apply appearance settings to the tab manager.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
settings: Dictionary containing appearance settings.
|
||||||
|
|
||||||
|
"""
|
||||||
|
self.settings = settings
|
||||||
|
bgcolor = settings.get("page_bgcolor", "#000000")
|
||||||
|
self.content_container.bgcolor = bgcolor
|
||||||
|
|
||||||
|
horizontal_scroll = settings.get("horizontal_scroll", False)
|
||||||
|
scroll_mode = ft.ScrollMode.ALWAYS if horizontal_scroll else ft.ScrollMode.AUTO
|
||||||
|
|
||||||
|
for tab in self.manager.tabs:
|
||||||
|
if "content" in tab and hasattr(tab["content"], "scroll"):
|
||||||
|
tab["content"].scroll = scroll_mode
|
||||||
|
if "content_control" in tab and hasattr(tab["content_control"], "scroll"):
|
||||||
|
tab["content_control"].scroll = scroll_mode
|
||||||
|
|
||||||
|
if self.content_container.content:
|
||||||
|
self.content_container.content.update()
|
||||||
|
self.page.update()
|
||||||
|
|
||||||
|
def _update_tab_visibility(self) -> None:
|
||||||
|
"""Dynamically adjust tab visibility based on page width.
|
||||||
|
|
||||||
|
Hides tabs that do not fit and moves them to an overflow menu.
|
||||||
|
"""
|
||||||
|
if not self.page.width or self.page.width == 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
if self.overflow_menu and self.overflow_menu in self.tab_bar.content.controls:
|
||||||
|
self.tab_bar.content.controls.remove(self.overflow_menu)
|
||||||
|
self.overflow_menu = None
|
||||||
|
|
||||||
|
available_width = self.page.width - 100
|
||||||
|
|
||||||
|
cumulative_width = 0
|
||||||
|
visible_tabs_count = 0
|
||||||
|
|
||||||
|
tab_containers = [
|
||||||
|
c for c in self.tab_bar.content.controls if isinstance(c, ft.Container)
|
||||||
|
]
|
||||||
|
|
||||||
|
for i, tab in enumerate(self.manager.tabs):
|
||||||
|
estimated_width = len(tab["title"]) * 10 + 32 + self.tab_bar.content.spacing
|
||||||
|
|
||||||
|
if cumulative_width + estimated_width <= available_width or i == 0:
|
||||||
|
cumulative_width += estimated_width
|
||||||
|
if i < len(tab_containers):
|
||||||
|
tab_containers[i].visible = True
|
||||||
|
visible_tabs_count += 1
|
||||||
|
elif i < len(tab_containers):
|
||||||
|
tab_containers[i].visible = False
|
||||||
|
|
||||||
|
if len(self.manager.tabs) > visible_tabs_count:
|
||||||
|
overflow_items = []
|
||||||
|
for i in range(visible_tabs_count, len(self.manager.tabs)):
|
||||||
|
tab_data = self.manager.tabs[i]
|
||||||
|
overflow_items.append(
|
||||||
|
ft.PopupMenuItem(
|
||||||
|
text=tab_data["title"],
|
||||||
|
on_click=lambda e, idx=i: self.select_tab(idx), # type: ignore
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
self.overflow_menu = ft.PopupMenuButton(
|
||||||
|
icon=ft.Icons.MORE_HORIZ,
|
||||||
|
tooltip=f"{len(self.manager.tabs) - visible_tabs_count} more tabs",
|
||||||
|
items=overflow_items,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.tab_bar.content.controls.insert(visible_tabs_count, self.overflow_menu)
|
||||||
|
|
||||||
|
def _add_tab_internal(self, title: str, content: ft.Control) -> None:
|
||||||
|
"""Add a new tab to the manager with the given title and content."""
|
||||||
idx = len(self.manager.tabs)
|
idx = len(self.manager.tabs)
|
||||||
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,
|
||||||
],
|
],
|
||||||
@@ -76,25 +203,49 @@ class TabsManager:
|
|||||||
"go_btn": go_btn,
|
"go_btn": go_btn,
|
||||||
"content_control": content_control,
|
"content_control": content_control,
|
||||||
"content": tab_content,
|
"content": tab_content,
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
btn = ft.Container(
|
tab_container = ft.Container(
|
||||||
content=ft.Text(title),
|
content=ft.Row(
|
||||||
on_click=lambda e, i=idx: self.select_tab(i),
|
controls=[
|
||||||
padding=ft.padding.symmetric(horizontal=12, vertical=6),
|
ft.Text(
|
||||||
border_radius=5,
|
title,
|
||||||
bgcolor=ft.Colors.SURFACE_CONTAINER_HIGHEST,
|
size=13,
|
||||||
|
weight=ft.FontWeight.W_500,
|
||||||
|
overflow=ft.TextOverflow.ELLIPSIS,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
spacing=8,
|
||||||
|
),
|
||||||
|
on_click=lambda e, i=idx: self.select_tab(i), # type: ignore
|
||||||
|
padding=ft.padding.symmetric(horizontal=16, vertical=10),
|
||||||
|
border_radius=8,
|
||||||
|
bgcolor=ft.Colors.GREY_800,
|
||||||
|
ink=True,
|
||||||
|
width=150,
|
||||||
)
|
)
|
||||||
insert_pos = max(0, len(self.tab_bar.controls) - 2)
|
insert_pos = max(0, len(self.tab_bar.content.controls) - 2)
|
||||||
self.tab_bar.controls.insert(insert_pos, btn)
|
self.tab_bar.content.controls.insert(insert_pos, tab_container)
|
||||||
|
self._update_tab_visibility()
|
||||||
|
|
||||||
def _on_add_click(self, e):
|
def _on_add_click(self, e) -> None: # type: ignore
|
||||||
|
"""Handle the add tab button click event."""
|
||||||
title = f"Tab {len(self.manager.tabs) + 1}"
|
title = f"Tab {len(self.manager.tabs) + 1}"
|
||||||
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)
|
||||||
)
|
)
|
||||||
@@ -102,19 +253,32 @@ class TabsManager:
|
|||||||
self.select_tab(len(self.manager.tabs) - 1)
|
self.select_tab(len(self.manager.tabs) - 1)
|
||||||
self.page.update()
|
self.page.update()
|
||||||
|
|
||||||
def _on_close_click(self, e):
|
def _on_close_click(self, e) -> None: # type: ignore
|
||||||
|
"""Handle the close tab button click event."""
|
||||||
if len(self.manager.tabs) <= 1:
|
if len(self.manager.tabs) <= 1:
|
||||||
return
|
return
|
||||||
idx = self.manager.index
|
idx = self.manager.index
|
||||||
|
|
||||||
|
tab_containers = [
|
||||||
|
c for c in self.tab_bar.content.controls if isinstance(c, ft.Container)
|
||||||
|
]
|
||||||
|
control_to_remove = tab_containers[idx]
|
||||||
|
|
||||||
self.manager.tabs.pop(idx)
|
self.manager.tabs.pop(idx)
|
||||||
self.tab_bar.controls.pop(idx)
|
self.tab_bar.content.controls.remove(control_to_remove)
|
||||||
for i, control in enumerate(self.tab_bar.controls[:-2]):
|
|
||||||
control.on_click = lambda e, i=i: self.select_tab(i)
|
updated_tab_containers = [
|
||||||
|
c for c in self.tab_bar.content.controls if isinstance(c, ft.Container)
|
||||||
|
]
|
||||||
|
for i, control in enumerate(updated_tab_containers):
|
||||||
|
control.on_click = lambda e, i=i: self.select_tab(i) # type: ignore
|
||||||
|
|
||||||
new_idx = min(idx, len(self.manager.tabs) - 1)
|
new_idx = min(idx, len(self.manager.tabs) - 1)
|
||||||
self.select_tab(new_idx)
|
self.select_tab(new_idx)
|
||||||
|
self._update_tab_visibility()
|
||||||
self.page.update()
|
self.page.update()
|
||||||
|
|
||||||
def select_tab(self, idx: int):
|
def select_tab(self, idx: int) -> None:
|
||||||
"""Select and display the tab at the given index.
|
"""Select and display the tab at the given index.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@@ -122,29 +286,89 @@ class TabsManager:
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
self.manager.index = idx
|
self.manager.index = idx
|
||||||
for i, control in enumerate(self.tab_bar.controls[:-2]):
|
|
||||||
|
tab_containers = [
|
||||||
|
c for c in self.tab_bar.content.controls if isinstance(c, ft.Container)
|
||||||
|
]
|
||||||
|
for i, control in enumerate(tab_containers):
|
||||||
if i == idx:
|
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()
|
||||||
|
|
||||||
def _on_tab_go(self, e, idx: int):
|
def _on_tab_go(self, e, idx: int) -> None: # type: ignore
|
||||||
|
"""Handle the go button click event for a tab, loading new content."""
|
||||||
tab = self.manager.tabs[idx]
|
tab = self.manager.tabs[idx]
|
||||||
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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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."""
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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."""
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ class TestTabsManager:
|
|||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def tabs_manager(self, mock_page):
|
def tabs_manager(self, mock_page):
|
||||||
"""Create a TabsManager instance for testing."""
|
"""Create a TabsManager instance for testing."""
|
||||||
|
mock_page.width = 800 # Simulate page width for adaptive logic
|
||||||
with (
|
with (
|
||||||
patch("ren_browser.app.RENDERER", "plaintext"),
|
patch("ren_browser.app.RENDERER", "plaintext"),
|
||||||
patch("ren_browser.renderer.plaintext.render_plaintext") as mock_render,
|
patch("ren_browser.renderer.plaintext.render_plaintext") as mock_render,
|
||||||
@@ -33,7 +34,9 @@ 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 isinstance(manager.tab_bar.content, ft.Row)
|
||||||
|
assert manager.overflow_menu is None
|
||||||
assert isinstance(manager.content_container, ft.Container)
|
assert isinstance(manager.content_container, ft.Container)
|
||||||
|
|
||||||
def test_tabs_manager_init_micron_renderer(self, mock_page):
|
def test_tabs_manager_init_micron_renderer(self, mock_page):
|
||||||
@@ -102,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."""
|
||||||
@@ -143,14 +148,14 @@ 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
|
# Should have: home tab, add button, close button (and potentially overflow menu)
|
||||||
assert len(controls) >= 3
|
assert len(controls) >= 3
|
||||||
assert isinstance(controls[-2], ft.IconButton) # Add button
|
assert isinstance(controls[-2], ft.IconButton) # Add button
|
||||||
assert isinstance(controls[-1], ft.IconButton) # Close button
|
assert isinstance(controls[-1], ft.IconButton) # Close button
|
||||||
@@ -177,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):
|
||||||
@@ -185,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
|
||||||
@@ -233,3 +240,34 @@ class TestTabsManager:
|
|||||||
tabs_manager.content_container.content
|
tabs_manager.content_container.content
|
||||||
== tabs_manager.manager.tabs[2]["content"]
|
== tabs_manager.manager.tabs[2]["content"]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_adaptive_overflow_behavior(self, tabs_manager):
|
||||||
|
"""Test that the overflow menu adapts to tab changes."""
|
||||||
|
# With page width at 800, add enough tabs that some should overflow.
|
||||||
|
for i in range(10): # Total 11 tabs
|
||||||
|
tabs_manager._add_tab_internal(f"Tab {i + 2}", Mock())
|
||||||
|
|
||||||
|
# Check that an overflow menu exists
|
||||||
|
assert tabs_manager.overflow_menu is not None
|
||||||
|
|
||||||
|
# Simulate a smaller screen, expecting more tabs to overflow
|
||||||
|
tabs_manager.page.width = 400
|
||||||
|
tabs_manager._update_tab_visibility()
|
||||||
|
visible_tabs_small = sum(
|
||||||
|
1
|
||||||
|
for c in tabs_manager.tab_bar.content.controls
|
||||||
|
if isinstance(c, ft.Container) and c.visible
|
||||||
|
)
|
||||||
|
assert visible_tabs_small < 11
|
||||||
|
|
||||||
|
# Simulate a larger screen, expecting all tabs to be visible
|
||||||
|
tabs_manager.page.width = 1600
|
||||||
|
tabs_manager._update_tab_visibility()
|
||||||
|
visible_tabs_large = sum(
|
||||||
|
1
|
||||||
|
for c in tabs_manager.tab_bar.content.controls
|
||||||
|
if isinstance(c, ft.Container) and c.visible
|
||||||
|
)
|
||||||
|
|
||||||
|
assert visible_tabs_large == 11
|
||||||
|
assert tabs_manager.overflow_menu is None
|
||||||
|
|||||||
@@ -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
327
uv.lock
generated
@@ -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" },
|
||||||
|
|||||||
Reference in New Issue
Block a user