Compare commits
34 Commits
v0.2.0
...
Tab-Overha
| Author | SHA1 | Date | |
|---|---|---|---|
| 6b5d476d74 | |||
| 5d640032ee | |||
| 0d2d595867 | |||
| 0b531bba54 | |||
| 3809ac8274 | |||
| a32a542c54 | |||
| e77faa5105 | |||
| c0f60d52db | |||
| 57c6b8ce3d | |||
| 9fc912fba4 | |||
| 0d878e8491 | |||
| 2796059aef | |||
| a5ae444b6c | |||
| d354b96334 | |||
| f511b60361 | |||
| b8386a60c6 | |||
| e20d6fe214 | |||
| 8b45e5d72b | |||
| 047169f3af | |||
| e0939e70f8 | |||
| 63e93d0cff | |||
| 926b3a198d | |||
| 8db441612f | |||
| b34b8f23ff | |||
| 13ad0bcef6 | |||
| 64b9ac3df4 | |||
| ee521a9f60 | |||
| fd4e0c8a14 | |||
| b056271da7 | |||
| 189256edd7 | |||
| 62d3502f99 | |||
| cb218f2b29 | |||
| 871f626555 | |||
| 9a20152a70 |
47
.github/workflows/build.yml
vendored
47
.github/workflows/build.yml
vendored
@@ -4,6 +4,7 @@ on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*.*.*'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build-linux:
|
||||
@@ -20,7 +21,7 @@ jobs:
|
||||
sudo apt-get install -y libgtk-3-dev cmake ninja-build clang pkg-config libgtk-3-dev liblzma-dev
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065
|
||||
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
|
||||
with:
|
||||
python-version: '3.13'
|
||||
|
||||
@@ -49,7 +50,7 @@ jobs:
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8
|
||||
|
||||
- name: Set up JDK
|
||||
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00
|
||||
uses: actions/setup-java@dded0888837ed1f317902acf8a20df0ad188d165 # v5.0.0
|
||||
with:
|
||||
distribution: 'zulu'
|
||||
java-version: '17'
|
||||
@@ -60,7 +61,7 @@ jobs:
|
||||
sudo apt-get install -y cmake ninja-build clang pkg-config
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065
|
||||
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
|
||||
with:
|
||||
python-version: '3.13'
|
||||
|
||||
@@ -78,4 +79,42 @@ jobs:
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02
|
||||
with:
|
||||
name: ren-browser-apk
|
||||
path: build/apk
|
||||
path: build/apk
|
||||
|
||||
create-release:
|
||||
needs: [build-linux, build-android]
|
||||
runs-on: ubuntu-latest
|
||||
if: startsWith(github.ref, 'refs/tags/')
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8
|
||||
|
||||
- name: Download Linux artifact
|
||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0
|
||||
with:
|
||||
name: ren-browser-linux
|
||||
path: ./artifacts/linux
|
||||
|
||||
- name: Download APK artifact
|
||||
uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0
|
||||
with:
|
||||
name: ren-browser-apk
|
||||
path: ./artifacts/apk
|
||||
|
||||
- name: Create draft release
|
||||
uses: softprops/action-gh-release@6cbd405e2c4e67a21c47fa9e383d020e4e28b836
|
||||
if: github.ref_type == 'tag'
|
||||
with:
|
||||
draft: true
|
||||
files: |
|
||||
./artifacts/linux/*
|
||||
./artifacts/apk/*
|
||||
name: Release ${{ github.ref_name }}
|
||||
body: |
|
||||
## Release ${{ github.ref_name }}
|
||||
|
||||
This release contains:
|
||||
- Linux binary package
|
||||
- Android APK package
|
||||
2
.github/workflows/docker.yml
vendored
2
.github/workflows/docker.yml
vendored
@@ -44,7 +44,7 @@ jobs:
|
||||
type=sha,format=short
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@ca052bb54ab0790a636c9b5f226502c73d547a25
|
||||
uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
|
||||
with:
|
||||
context: .
|
||||
file: Dockerfile
|
||||
|
||||
2
.github/workflows/safety.yml
vendored
2
.github/workflows/safety.yml
vendored
@@ -12,6 +12,6 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8
|
||||
- name: Run Safety CLI to check for vulnerabilities
|
||||
uses: pyupio/safety-action@7baf6605473beffc874c1313ddf2db085c0cacf2
|
||||
uses: pyupio/safety-action@2591cf2f3e67ba68b923f4c92f0d36e281c65023 # v1.0.1
|
||||
with:
|
||||
api-key: ${{ secrets.SAFETY_API_KEY }}
|
||||
|
||||
6
.github/workflows/test.yml
vendored
6
.github/workflows/test.yml
vendored
@@ -24,7 +24,7 @@ jobs:
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8
|
||||
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@7f4fc3e22c37d6ff65e88745f38bd3157c663f7c
|
||||
uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
|
||||
@@ -44,7 +44,7 @@ jobs:
|
||||
poetry config virtualenvs.in-project true
|
||||
|
||||
- name: Cache Poetry dependencies
|
||||
uses: actions/cache@2f8e54208210a422b2efd51efaa6bd6d7ca8920f
|
||||
uses: actions/cache@0400d5f644dc74513175e3cd8d07132dd4860809 # v4.2.4
|
||||
with:
|
||||
path: .venv
|
||||
key: venv-${{ runner.os }}-${{ matrix.python-version }}-${{ hashFiles('**/poetry.lock') }}
|
||||
@@ -64,7 +64,7 @@ jobs:
|
||||
poetry run pytest -v --cov=ren_browser --cov-report=xml --cov-report=term
|
||||
|
||||
- name: Upload coverage reports
|
||||
uses: codecov/codecov-action@ab904c41d6ece82784817410c45d8b8c02684457
|
||||
uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1
|
||||
if: matrix.python-version == '3.13'
|
||||
with:
|
||||
file: ./coverage.xml
|
||||
|
||||
@@ -7,13 +7,51 @@ I welcome all contributions to the project.
|
||||
- Styling/Design (I am bad at this)
|
||||
- Documentation
|
||||
- Micron Renderer/Parser
|
||||
- Android and Flet (config/permissions/etc)
|
||||
|
||||
## Project Structure
|
||||
|
||||
Last Updated: 2025-09-28
|
||||
|
||||
```
|
||||
Ren-Browser/
|
||||
├── ren_browser/ # Main Python application package
|
||||
│ ├── announces/ # Reticulum network announce handling
|
||||
│ │ ├── announces.py
|
||||
│ ├── app.py # Main application entry point
|
||||
│ ├── controls/ # UI controls and interactions
|
||||
│ │ ├── shortcuts.py # Keyboard shortcuts handling
|
||||
│ ├── logs.py # Centralized logging system
|
||||
│ ├── pages/ # Page fetching and request handling
|
||||
│ │ ├── page_request.py
|
||||
│ ├── profiler/ # Performance profiling (placeholder)
|
||||
│ ├── renderer/ # Content rendering system
|
||||
│ │ ├── micron.py # Micron markup renderer (WIP)
|
||||
│ │ └── plaintext.py # Plaintext fallback renderer
|
||||
│ ├── storage/ # Cross-platform storage management
|
||||
│ │ ├── storage.py
|
||||
│ ├── tabs/ # Tab management system
|
||||
│ │ ├── tabs.py
|
||||
│ ├── ui/ # User interface components
|
||||
│ │ ├── settings.py # Settings interface
|
||||
│ │ └── ui.py # Main UI construction
|
||||
├── tests/ # Test suite
|
||||
│ ├── unit/ # Unit tests
|
||||
│ ├── integration/ # Integration tests
|
||||
│ └── conftest.py # Test configuration
|
||||
```
|
||||
|
||||
## Rules
|
||||
|
||||
1. Be nice to each other.
|
||||
2. If you use an AI tool that generates the code, such as a LLM, please indicate that in the PR.
|
||||
3. Add or update docstrings and tests if necessary.
|
||||
4. Make sure you run the tests before submitting the PR.
|
||||
|
||||
## Generative AI Usage
|
||||
|
||||
You are allowed to use generative AI tools to help learn and contribute. You do not need to disclose you used a AI tool, although that would help me scrutinize the PR more for bugs, errors or security flaws.
|
||||
|
||||
## Linting, Security and Tests
|
||||
|
||||
You are not required to run the linting, security and tests before submitting the PR as those will be run by the CI/CD pipeline.
|
||||
|
||||
## Testing
|
||||
|
||||
|
||||
35
Dockerfile
35
Dockerfile
@@ -1,18 +1,29 @@
|
||||
FROM python:3.13-alpine
|
||||
ARG PYTHON_VERSION=3.13
|
||||
FROM python:${PYTHON_VERSION}-alpine
|
||||
|
||||
# Install build dependencies for cryptography
|
||||
RUN apk add --no-cache gcc musl-dev libffi-dev openssl-dev
|
||||
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"
|
||||
|
||||
# Upgrade pip and install application dependencies
|
||||
RUN pip install --upgrade pip \
|
||||
&& pip install --no-cache-dir "flet>=0.28.3,<0.29.0" "rns>=0.9.6,<0.10.0"
|
||||
|
||||
# Copy application source
|
||||
WORKDIR /app
|
||||
COPY . /app
|
||||
|
||||
# Expose the web port
|
||||
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
|
||||
|
||||
# Run the web version of Ren Browser
|
||||
CMD ["python3", "-u", "-m", "ren_browser.app", "--web", "--port", "8550"]
|
||||
ENTRYPOINT ["poetry", "run", "ren-browser-web"]
|
||||
80
Makefile
Normal file
80
Makefile
Normal file
@@ -0,0 +1,80 @@
|
||||
# Ren Browser Makefile
|
||||
.PHONY: help build poetry-build linux apk docker-build docker-build-multi docker-run docker-stop clean test lint format
|
||||
|
||||
# Default target
|
||||
help:
|
||||
@echo "Ren Browser Build System"
|
||||
@echo ""
|
||||
@echo "Available targets:"
|
||||
@echo " build - Build the project (alias for poetry-build)"
|
||||
@echo " poetry-build - Build project with Poetry"
|
||||
@echo " linux - Build Linux package"
|
||||
@echo " apk - Build Android APK"
|
||||
@echo " docker-build - Build Docker image with Buildx"
|
||||
@echo " docker-build-multi - Build multi-platform Docker image"
|
||||
@echo " docker-run - Run Docker container"
|
||||
@echo " docker-stop - Stop Docker container"
|
||||
@echo " test - Run tests"
|
||||
@echo " lint - Run linter"
|
||||
@echo " format - Format code"
|
||||
@echo " clean - Clean build artifacts"
|
||||
@echo " help - Show this help"
|
||||
|
||||
# Main build target
|
||||
build: poetry-build
|
||||
|
||||
# Poetry build
|
||||
poetry-build:
|
||||
@echo "Building project with Poetry..."
|
||||
poetry build
|
||||
|
||||
# Linux package build
|
||||
linux:
|
||||
@echo "Building Linux package..."
|
||||
poetry run flet build linux
|
||||
|
||||
# Android APK build
|
||||
apk:
|
||||
@echo "Building Android APK..."
|
||||
poetry run flet build apk
|
||||
|
||||
# Docker targets
|
||||
docker-build:
|
||||
@echo "Building Docker image with Buildx..."
|
||||
docker buildx build -t ren-browser --load .
|
||||
|
||||
docker-build-multi:
|
||||
@echo "Building multi-platform Docker image..."
|
||||
docker buildx build -t ren-browser-multi --platform linux/amd64,linux/arm64 --push .
|
||||
|
||||
docker-run:
|
||||
@echo "Running Docker container..."
|
||||
docker run -p 8550:8550 --name ren-browser-container ren-browser
|
||||
|
||||
docker-stop:
|
||||
@echo "Stopping Docker container..."
|
||||
docker stop ren-browser-container || true
|
||||
docker rm ren-browser-container || true
|
||||
|
||||
# Development targets
|
||||
test:
|
||||
@echo "Running tests..."
|
||||
poetry run pytest
|
||||
|
||||
lint:
|
||||
@echo "Running linter..."
|
||||
poetry run ruff check .
|
||||
|
||||
format:
|
||||
@echo "Formatting code..."
|
||||
poetry run ruff format .
|
||||
|
||||
# Clean build artifacts
|
||||
clean:
|
||||
@echo "Cleaning build artifacts..."
|
||||
rm -rf build/
|
||||
rm -rf dist/
|
||||
rm -rf *.egg-info/
|
||||
find . -type d -name __pycache__ -exec rm -rf {} +
|
||||
find . -type f -name "*.pyc" -delete
|
||||
docker rmi ren-browser || true
|
||||
40
README.md
40
README.md
@@ -1,13 +1,14 @@
|
||||
# Ren Browser
|
||||
|
||||
A browser for the [Reticulum Network](https://reticulum.network/). Work-in-progress.
|
||||
A browser for the [Reticulum Network](https://reticulum.network/).
|
||||
|
||||
> [!WARNING]
|
||||
> This is a work-in-progress.
|
||||
|
||||
Target platforms: Web, Linux, Windows, MacOS, Android, iOS.
|
||||
|
||||
Built using [Flet](https://flet.dev/).
|
||||
|
||||
Currently, you can find `Linux` and `Android` builds in action artifacts in the [GitHub Actions](https://github.com/Sudo-Ivan/Ren-Browser/actions/workflows/build.yml) page, click on the latest workflow run. More platforms will be added in the future.
|
||||
|
||||
## Renderers
|
||||
|
||||
- Micron (default) (WIP)
|
||||
@@ -20,24 +21,26 @@ Currently, you can find `Linux` and `Android` builds in action artifacts in the
|
||||
- Python 3.13+
|
||||
- Flet
|
||||
- Reticulum 1.0.0+
|
||||
- Poetry
|
||||
- UV
|
||||
|
||||
**Setup**
|
||||
|
||||
```bash
|
||||
poetry install
|
||||
uv sync
|
||||
```
|
||||
|
||||
### Desktop
|
||||
|
||||
```bash
|
||||
poetry run ren-browser-dev
|
||||
# From local development
|
||||
uv run ren-browser
|
||||
```
|
||||
|
||||
### Web
|
||||
|
||||
```bash
|
||||
poetry run ren-browser-web-dev
|
||||
# From local development
|
||||
uv run ren-browser-web
|
||||
```
|
||||
|
||||
### Mobile
|
||||
@@ -45,13 +48,28 @@ poetry run ren-browser-web-dev
|
||||
**Android**
|
||||
|
||||
```bash
|
||||
poetry run ren-browser-android-dev
|
||||
# From local development
|
||||
uv run ren-browser-android
|
||||
```
|
||||
|
||||
**iOS**
|
||||
|
||||
```bash
|
||||
poetry run ren-browser-ios-dev
|
||||
# From local development
|
||||
uv run ren-browser-ios
|
||||
```
|
||||
|
||||
To run directly from the GitHub repository without cloning:
|
||||
|
||||
```bash
|
||||
# Using uvx (temporary environment)
|
||||
uvx --from git+https://github.com/Sudo-Ivan/Ren-Browser.git ren-browser-web
|
||||
|
||||
# Or clone and run locally
|
||||
git clone https://github.com/Sudo-Ivan/Ren-Browser.git
|
||||
cd Ren-Browser
|
||||
uv sync
|
||||
uv run ren-browser-web
|
||||
```
|
||||
|
||||
### Docker/Podman
|
||||
@@ -66,11 +84,11 @@ docker run -p 8550:8550 -v ./config:/app/config ren-browser
|
||||
### Linux
|
||||
|
||||
```bash
|
||||
poetry run flet build linux
|
||||
uv run flet build linux
|
||||
```
|
||||
|
||||
### Android
|
||||
|
||||
```bash
|
||||
poetry run flet build android
|
||||
uv run flet build android
|
||||
```
|
||||
375
poetry.lock
generated
375
poetry.lock
generated
@@ -1,16 +1,16 @@
|
||||
# This file is automatically @generated by Poetry 2.1.2 and should not be changed by hand.
|
||||
# This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand.
|
||||
|
||||
[[package]]
|
||||
name = "anyio"
|
||||
version = "4.10.0"
|
||||
version = "4.11.0"
|
||||
description = "High-level concurrency and networking framework on top of asyncio or Trio"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main"]
|
||||
markers = "platform_system != \"Pyodide\""
|
||||
files = [
|
||||
{file = "anyio-4.10.0-py3-none-any.whl", hash = "sha256:60e474ac86736bbfd6f210f7a61218939c318f43f9972497381f1c5e930ed3d1"},
|
||||
{file = "anyio-4.10.0.tar.gz", hash = "sha256:3f3fae35c96039744587aa5b8371e7e8e603c0702999535961dd336026973ba6"},
|
||||
{file = "anyio-4.11.0-py3-none-any.whl", hash = "sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc"},
|
||||
{file = "anyio-4.11.0.tar.gz", hash = "sha256:82a8d0b81e318cc5ce71a5f1f8b5c4e63619620b63141ef8c995fa0db95a57c4"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -18,19 +18,19 @@ idna = ">=2.8"
|
||||
sniffio = ">=1.1"
|
||||
|
||||
[package.extras]
|
||||
trio = ["trio (>=0.26.1)"]
|
||||
trio = ["trio (>=0.31.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "certifi"
|
||||
version = "2025.8.3"
|
||||
version = "2025.10.5"
|
||||
description = "Python package for providing Mozilla's CA Bundle."
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
groups = ["main"]
|
||||
markers = "platform_system != \"Pyodide\""
|
||||
files = [
|
||||
{file = "certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5"},
|
||||
{file = "certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407"},
|
||||
{file = "certifi-2025.10.5-py3-none-any.whl", hash = "sha256:0f212c2744a9bb6de0c56639a6f68afe01ecd92d91f14ae897c4fe7bbeeef0de"},
|
||||
{file = "certifi-2025.10.5.tar.gz", hash = "sha256:47c09d31ccf2acf0be3f701ea53595ee7e0b8fa08801c6624be771df09ae7b43"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -146,100 +146,104 @@ files = [
|
||||
|
||||
[[package]]
|
||||
name = "coverage"
|
||||
version = "7.10.6"
|
||||
version = "7.11.0"
|
||||
description = "Code coverage measurement for Python"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
python-versions = ">=3.10"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "coverage-7.10.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:70e7bfbd57126b5554aa482691145f798d7df77489a177a6bef80de78860a356"},
|
||||
{file = "coverage-7.10.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e41be6f0f19da64af13403e52f2dec38bbc2937af54df8ecef10850ff8d35301"},
|
||||
{file = "coverage-7.10.6-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c61fc91ab80b23f5fddbee342d19662f3d3328173229caded831aa0bd7595460"},
|
||||
{file = "coverage-7.10.6-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:10356fdd33a7cc06e8051413140bbdc6f972137508a3572e3f59f805cd2832fd"},
|
||||
{file = "coverage-7.10.6-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:80b1695cf7c5ebe7b44bf2521221b9bb8cdf69b1f24231149a7e3eb1ae5fa2fb"},
|
||||
{file = "coverage-7.10.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:2e4c33e6378b9d52d3454bd08847a8651f4ed23ddbb4a0520227bd346382bbc6"},
|
||||
{file = "coverage-7.10.6-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:c8a3ec16e34ef980a46f60dc6ad86ec60f763c3f2fa0db6d261e6e754f72e945"},
|
||||
{file = "coverage-7.10.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:7d79dabc0a56f5af990cc6da9ad1e40766e82773c075f09cc571e2076fef882e"},
|
||||
{file = "coverage-7.10.6-cp310-cp310-win32.whl", hash = "sha256:86b9b59f2b16e981906e9d6383eb6446d5b46c278460ae2c36487667717eccf1"},
|
||||
{file = "coverage-7.10.6-cp310-cp310-win_amd64.whl", hash = "sha256:e132b9152749bd33534e5bd8565c7576f135f157b4029b975e15ee184325f528"},
|
||||
{file = "coverage-7.10.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c706db3cabb7ceef779de68270150665e710b46d56372455cd741184f3868d8f"},
|
||||
{file = "coverage-7.10.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8e0c38dc289e0508ef68ec95834cb5d2e96fdbe792eaccaa1bccac3966bbadcc"},
|
||||
{file = "coverage-7.10.6-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:752a3005a1ded28f2f3a6e8787e24f28d6abe176ca64677bcd8d53d6fe2ec08a"},
|
||||
{file = "coverage-7.10.6-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:689920ecfd60f992cafca4f5477d55720466ad2c7fa29bb56ac8d44a1ac2b47a"},
|
||||
{file = "coverage-7.10.6-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ec98435796d2624d6905820a42f82149ee9fc4f2d45c2c5bc5a44481cc50db62"},
|
||||
{file = "coverage-7.10.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b37201ce4a458c7a758ecc4efa92fa8ed783c66e0fa3c42ae19fc454a0792153"},
|
||||
{file = "coverage-7.10.6-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:2904271c80898663c810a6b067920a61dd8d38341244a3605bd31ab55250dad5"},
|
||||
{file = "coverage-7.10.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5aea98383463d6e1fa4e95416d8de66f2d0cb588774ee20ae1b28df826bcb619"},
|
||||
{file = "coverage-7.10.6-cp311-cp311-win32.whl", hash = "sha256:e3fb1fa01d3598002777dd259c0c2e6d9d5e10e7222976fc8e03992f972a2cba"},
|
||||
{file = "coverage-7.10.6-cp311-cp311-win_amd64.whl", hash = "sha256:f35ed9d945bece26553d5b4c8630453169672bea0050a564456eb88bdffd927e"},
|
||||
{file = "coverage-7.10.6-cp311-cp311-win_arm64.whl", hash = "sha256:99e1a305c7765631d74b98bf7dbf54eeea931f975e80f115437d23848ee8c27c"},
|
||||
{file = "coverage-7.10.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5b2dd6059938063a2c9fee1af729d4f2af28fd1a545e9b7652861f0d752ebcea"},
|
||||
{file = "coverage-7.10.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:388d80e56191bf846c485c14ae2bc8898aa3124d9d35903fef7d907780477634"},
|
||||
{file = "coverage-7.10.6-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:90cb5b1a4670662719591aa92d0095bb41714970c0b065b02a2610172dbf0af6"},
|
||||
{file = "coverage-7.10.6-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:961834e2f2b863a0e14260a9a273aff07ff7818ab6e66d2addf5628590c628f9"},
|
||||
{file = "coverage-7.10.6-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bf9a19f5012dab774628491659646335b1928cfc931bf8d97b0d5918dd58033c"},
|
||||
{file = "coverage-7.10.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:99c4283e2a0e147b9c9cc6bc9c96124de9419d6044837e9799763a0e29a7321a"},
|
||||
{file = "coverage-7.10.6-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:282b1b20f45df57cc508c1e033403f02283adfb67d4c9c35a90281d81e5c52c5"},
|
||||
{file = "coverage-7.10.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8cdbe264f11afd69841bd8c0d83ca10b5b32853263ee62e6ac6a0ab63895f972"},
|
||||
{file = "coverage-7.10.6-cp312-cp312-win32.whl", hash = "sha256:a517feaf3a0a3eca1ee985d8373135cfdedfbba3882a5eab4362bda7c7cf518d"},
|
||||
{file = "coverage-7.10.6-cp312-cp312-win_amd64.whl", hash = "sha256:856986eadf41f52b214176d894a7de05331117f6035a28ac0016c0f63d887629"},
|
||||
{file = "coverage-7.10.6-cp312-cp312-win_arm64.whl", hash = "sha256:acf36b8268785aad739443fa2780c16260ee3fa09d12b3a70f772ef100939d80"},
|
||||
{file = "coverage-7.10.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ffea0575345e9ee0144dfe5701aa17f3ba546f8c3bb48db62ae101afb740e7d6"},
|
||||
{file = "coverage-7.10.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:95d91d7317cde40a1c249d6b7382750b7e6d86fad9d8eaf4fa3f8f44cf171e80"},
|
||||
{file = "coverage-7.10.6-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3e23dd5408fe71a356b41baa82892772a4cefcf758f2ca3383d2aa39e1b7a003"},
|
||||
{file = "coverage-7.10.6-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:0f3f56e4cb573755e96a16501a98bf211f100463d70275759e73f3cbc00d4f27"},
|
||||
{file = "coverage-7.10.6-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:db4a1d897bbbe7339946ffa2fe60c10cc81c43fab8b062d3fcb84188688174a4"},
|
||||
{file = "coverage-7.10.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d8fd7879082953c156d5b13c74aa6cca37f6a6f4747b39538504c3f9c63d043d"},
|
||||
{file = "coverage-7.10.6-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:28395ca3f71cd103b8c116333fa9db867f3a3e1ad6a084aa3725ae002b6583bc"},
|
||||
{file = "coverage-7.10.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:61c950fc33d29c91b9e18540e1aed7d9f6787cc870a3e4032493bbbe641d12fc"},
|
||||
{file = "coverage-7.10.6-cp313-cp313-win32.whl", hash = "sha256:160c00a5e6b6bdf4e5984b0ef21fc860bc94416c41b7df4d63f536d17c38902e"},
|
||||
{file = "coverage-7.10.6-cp313-cp313-win_amd64.whl", hash = "sha256:628055297f3e2aa181464c3808402887643405573eb3d9de060d81531fa79d32"},
|
||||
{file = "coverage-7.10.6-cp313-cp313-win_arm64.whl", hash = "sha256:df4ec1f8540b0bcbe26ca7dd0f541847cc8a108b35596f9f91f59f0c060bfdd2"},
|
||||
{file = "coverage-7.10.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:c9a8b7a34a4de3ed987f636f71881cd3b8339f61118b1aa311fbda12741bff0b"},
|
||||
{file = "coverage-7.10.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8dd5af36092430c2b075cee966719898f2ae87b636cefb85a653f1d0ba5d5393"},
|
||||
{file = "coverage-7.10.6-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b0353b0f0850d49ada66fdd7d0c7cdb0f86b900bb9e367024fd14a60cecc1e27"},
|
||||
{file = "coverage-7.10.6-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d6b9ae13d5d3e8aeca9ca94198aa7b3ebbc5acfada557d724f2a1f03d2c0b0df"},
|
||||
{file = "coverage-7.10.6-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:675824a363cc05781b1527b39dc2587b8984965834a748177ee3c37b64ffeafb"},
|
||||
{file = "coverage-7.10.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:692d70ea725f471a547c305f0d0fc6a73480c62fb0da726370c088ab21aed282"},
|
||||
{file = "coverage-7.10.6-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:851430a9a361c7a8484a36126d1d0ff8d529d97385eacc8dfdc9bfc8c2d2cbe4"},
|
||||
{file = "coverage-7.10.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d9369a23186d189b2fc95cc08b8160ba242057e887d766864f7adf3c46b2df21"},
|
||||
{file = "coverage-7.10.6-cp313-cp313t-win32.whl", hash = "sha256:92be86fcb125e9bda0da7806afd29a3fd33fdf58fba5d60318399adf40bf37d0"},
|
||||
{file = "coverage-7.10.6-cp313-cp313t-win_amd64.whl", hash = "sha256:6b3039e2ca459a70c79523d39347d83b73f2f06af5624905eba7ec34d64d80b5"},
|
||||
{file = "coverage-7.10.6-cp313-cp313t-win_arm64.whl", hash = "sha256:3fb99d0786fe17b228eab663d16bee2288e8724d26a199c29325aac4b0319b9b"},
|
||||
{file = "coverage-7.10.6-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:6008a021907be8c4c02f37cdc3ffb258493bdebfeaf9a839f9e71dfdc47b018e"},
|
||||
{file = "coverage-7.10.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:5e75e37f23eb144e78940b40395b42f2321951206a4f50e23cfd6e8a198d3ceb"},
|
||||
{file = "coverage-7.10.6-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0f7cb359a448e043c576f0da00aa8bfd796a01b06aa610ca453d4dde09cc1034"},
|
||||
{file = "coverage-7.10.6-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c68018e4fc4e14b5668f1353b41ccf4bc83ba355f0e1b3836861c6f042d89ac1"},
|
||||
{file = "coverage-7.10.6-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cd4b2b0707fc55afa160cd5fc33b27ccbf75ca11d81f4ec9863d5793fc6df56a"},
|
||||
{file = "coverage-7.10.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:4cec13817a651f8804a86e4f79d815b3b28472c910e099e4d5a0e8a3b6a1d4cb"},
|
||||
{file = "coverage-7.10.6-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:f2a6a8e06bbda06f78739f40bfb56c45d14eb8249d0f0ea6d4b3d48e1f7c695d"},
|
||||
{file = "coverage-7.10.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:081b98395ced0d9bcf60ada7661a0b75f36b78b9d7e39ea0790bb4ed8da14747"},
|
||||
{file = "coverage-7.10.6-cp314-cp314-win32.whl", hash = "sha256:6937347c5d7d069ee776b2bf4e1212f912a9f1f141a429c475e6089462fcecc5"},
|
||||
{file = "coverage-7.10.6-cp314-cp314-win_amd64.whl", hash = "sha256:adec1d980fa07e60b6ef865f9e5410ba760e4e1d26f60f7e5772c73b9a5b0713"},
|
||||
{file = "coverage-7.10.6-cp314-cp314-win_arm64.whl", hash = "sha256:a80f7aef9535442bdcf562e5a0d5a5538ce8abe6bb209cfbf170c462ac2c2a32"},
|
||||
{file = "coverage-7.10.6-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:0de434f4fbbe5af4fa7989521c655c8c779afb61c53ab561b64dcee6149e4c65"},
|
||||
{file = "coverage-7.10.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6e31b8155150c57e5ac43ccd289d079eb3f825187d7c66e755a055d2c85794c6"},
|
||||
{file = "coverage-7.10.6-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:98cede73eb83c31e2118ae8d379c12e3e42736903a8afcca92a7218e1f2903b0"},
|
||||
{file = "coverage-7.10.6-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f863c08f4ff6b64fa8045b1e3da480f5374779ef187f07b82e0538c68cb4ff8e"},
|
||||
{file = "coverage-7.10.6-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2b38261034fda87be356f2c3f42221fdb4171c3ce7658066ae449241485390d5"},
|
||||
{file = "coverage-7.10.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0e93b1476b79eae849dc3872faeb0bf7948fd9ea34869590bc16a2a00b9c82a7"},
|
||||
{file = "coverage-7.10.6-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ff8a991f70f4c0cf53088abf1e3886edcc87d53004c7bb94e78650b4d3dac3b5"},
|
||||
{file = "coverage-7.10.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ac765b026c9f33044419cbba1da913cfb82cca1b60598ac1c7a5ed6aac4621a0"},
|
||||
{file = "coverage-7.10.6-cp314-cp314t-win32.whl", hash = "sha256:441c357d55f4936875636ef2cfb3bee36e466dcf50df9afbd398ce79dba1ebb7"},
|
||||
{file = "coverage-7.10.6-cp314-cp314t-win_amd64.whl", hash = "sha256:073711de3181b2e204e4870ac83a7c4853115b42e9cd4d145f2231e12d670930"},
|
||||
{file = "coverage-7.10.6-cp314-cp314t-win_arm64.whl", hash = "sha256:137921f2bac5559334ba66122b753db6dc5d1cf01eb7b64eb412bb0d064ef35b"},
|
||||
{file = "coverage-7.10.6-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:90558c35af64971d65fbd935c32010f9a2f52776103a259f1dee865fe8259352"},
|
||||
{file = "coverage-7.10.6-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8953746d371e5695405806c46d705a3cd170b9cc2b9f93953ad838f6c1e58612"},
|
||||
{file = "coverage-7.10.6-cp39-cp39-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c83f6afb480eae0313114297d29d7c295670a41c11b274e6bca0c64540c1ce7b"},
|
||||
{file = "coverage-7.10.6-cp39-cp39-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7eb68d356ba0cc158ca535ce1381dbf2037fa8cb5b1ae5ddfc302e7317d04144"},
|
||||
{file = "coverage-7.10.6-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5b15a87265e96307482746d86995f4bff282f14b027db75469c446da6127433b"},
|
||||
{file = "coverage-7.10.6-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fc53ba868875bfbb66ee447d64d6413c2db91fddcfca57025a0e7ab5b07d5862"},
|
||||
{file = "coverage-7.10.6-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:efeda443000aa23f276f4df973cb82beca682fd800bb119d19e80504ffe53ec2"},
|
||||
{file = "coverage-7.10.6-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:9702b59d582ff1e184945d8b501ffdd08d2cee38d93a2206aa5f1365ce0b8d78"},
|
||||
{file = "coverage-7.10.6-cp39-cp39-win32.whl", hash = "sha256:2195f8e16ba1a44651ca684db2ea2b2d4b5345da12f07d9c22a395202a05b23c"},
|
||||
{file = "coverage-7.10.6-cp39-cp39-win_amd64.whl", hash = "sha256:f32ff80e7ef6a5b5b606ea69a36e97b219cd9dc799bcf2963018a4d8f788cfbf"},
|
||||
{file = "coverage-7.10.6-py3-none-any.whl", hash = "sha256:92c4ecf6bf11b2e85fd4d8204814dc26e6a19f0c9d938c207c5cb0eadfcabbe3"},
|
||||
{file = "coverage-7.10.6.tar.gz", hash = "sha256:f644a3ae5933a552a29dbb9aa2f90c677a875f80ebea028e5a52a4f429044b90"},
|
||||
{file = "coverage-7.11.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:eb53f1e8adeeb2e78962bade0c08bfdc461853c7969706ed901821e009b35e31"},
|
||||
{file = "coverage-7.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d9a03ec6cb9f40a5c360f138b88266fd8f58408d71e89f536b4f91d85721d075"},
|
||||
{file = "coverage-7.11.0-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:0d7f0616c557cbc3d1c2090334eddcbb70e1ae3a40b07222d62b3aa47f608fab"},
|
||||
{file = "coverage-7.11.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:e44a86a47bbdf83b0a3ea4d7df5410d6b1a0de984fbd805fa5101f3624b9abe0"},
|
||||
{file = "coverage-7.11.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:596763d2f9a0ee7eec6e643e29660def2eef297e1de0d334c78c08706f1cb785"},
|
||||
{file = "coverage-7.11.0-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ef55537ff511b5e0a43edb4c50a7bf7ba1c3eea20b4f49b1490f1e8e0e42c591"},
|
||||
{file = "coverage-7.11.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9cbabd8f4d0d3dc571d77ae5bdbfa6afe5061e679a9d74b6797c48d143307088"},
|
||||
{file = "coverage-7.11.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e24045453384e0ae2a587d562df2a04d852672eb63051d16096d3f08aa4c7c2f"},
|
||||
{file = "coverage-7.11.0-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:7161edd3426c8d19bdccde7d49e6f27f748f3c31cc350c5de7c633fea445d866"},
|
||||
{file = "coverage-7.11.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3d4ed4de17e692ba6415b0587bc7f12bc80915031fc9db46a23ce70fc88c9841"},
|
||||
{file = "coverage-7.11.0-cp310-cp310-win32.whl", hash = "sha256:765c0bc8fe46f48e341ef737c91c715bd2a53a12792592296a095f0c237e09cf"},
|
||||
{file = "coverage-7.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:24d6f3128f1b2d20d84b24f4074475457faedc3d4613a7e66b5e769939c7d969"},
|
||||
{file = "coverage-7.11.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3d58ecaa865c5b9fa56e35efc51d1014d4c0d22838815b9fce57a27dd9576847"},
|
||||
{file = "coverage-7.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b679e171f1c104a5668550ada700e3c4937110dbdd153b7ef9055c4f1a1ee3cc"},
|
||||
{file = "coverage-7.11.0-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ca61691ba8c5b6797deb221a0d09d7470364733ea9c69425a640f1f01b7c5bf0"},
|
||||
{file = "coverage-7.11.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:aef1747ede4bd8ca9cfc04cc3011516500c6891f1b33a94add3253f6f876b7b7"},
|
||||
{file = "coverage-7.11.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a1839d08406e4cba2953dcc0ffb312252f14d7c4c96919f70167611f4dee2623"},
|
||||
{file = "coverage-7.11.0-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e0eb0a2dcc62478eb5b4cbb80b97bdee852d7e280b90e81f11b407d0b81c4287"},
|
||||
{file = "coverage-7.11.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bc1fbea96343b53f65d5351d8fd3b34fd415a2670d7c300b06d3e14a5af4f552"},
|
||||
{file = "coverage-7.11.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:214b622259dd0cf435f10241f1333d32caa64dbc27f8790ab693428a141723de"},
|
||||
{file = "coverage-7.11.0-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:258d9967520cca899695d4eb7ea38be03f06951d6ca2f21fb48b1235f791e601"},
|
||||
{file = "coverage-7.11.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:cf9e6ff4ca908ca15c157c409d608da77a56a09877b97c889b98fb2c32b6465e"},
|
||||
{file = "coverage-7.11.0-cp311-cp311-win32.whl", hash = "sha256:fcc15fc462707b0680cff6242c48625da7f9a16a28a41bb8fd7a4280920e676c"},
|
||||
{file = "coverage-7.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:865965bf955d92790f1facd64fe7ff73551bd2c1e7e6b26443934e9701ba30b9"},
|
||||
{file = "coverage-7.11.0-cp311-cp311-win_arm64.whl", hash = "sha256:5693e57a065760dcbeb292d60cc4d0231a6d4b6b6f6a3191561e1d5e8820b745"},
|
||||
{file = "coverage-7.11.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9c49e77811cf9d024b95faf86c3f059b11c0c9be0b0d61bc598f453703bd6fd1"},
|
||||
{file = "coverage-7.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a61e37a403a778e2cda2a6a39abcc895f1d984071942a41074b5c7ee31642007"},
|
||||
{file = "coverage-7.11.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:c79cae102bb3b1801e2ef1511fb50e91ec83a1ce466b2c7c25010d884336de46"},
|
||||
{file = "coverage-7.11.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:16ce17ceb5d211f320b62df002fa7016b7442ea0fd260c11cec8ce7730954893"},
|
||||
{file = "coverage-7.11.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:80027673e9d0bd6aef86134b0771845e2da85755cf686e7c7c59566cf5a89115"},
|
||||
{file = "coverage-7.11.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:4d3ffa07a08657306cd2215b0da53761c4d73cb54d9143b9303a6481ec0cd415"},
|
||||
{file = "coverage-7.11.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a3b6a5f8b2524fd6c1066bc85bfd97e78709bb5e37b5b94911a6506b65f47186"},
|
||||
{file = "coverage-7.11.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:fcc0a4aa589de34bc56e1a80a740ee0f8c47611bdfb28cd1849de60660f3799d"},
|
||||
{file = "coverage-7.11.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:dba82204769d78c3fd31b35c3d5f46e06511936c5019c39f98320e05b08f794d"},
|
||||
{file = "coverage-7.11.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:81b335f03ba67309a95210caf3eb43bd6fe75a4e22ba653ef97b4696c56c7ec2"},
|
||||
{file = "coverage-7.11.0-cp312-cp312-win32.whl", hash = "sha256:037b2d064c2f8cc8716fe4d39cb705779af3fbf1ba318dc96a1af858888c7bb5"},
|
||||
{file = "coverage-7.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:d66c0104aec3b75e5fd897e7940188ea1892ca1d0235316bf89286d6a22568c0"},
|
||||
{file = "coverage-7.11.0-cp312-cp312-win_arm64.whl", hash = "sha256:d91ebeac603812a09cf6a886ba6e464f3bbb367411904ae3790dfe28311b15ad"},
|
||||
{file = "coverage-7.11.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:cc3f49e65ea6e0d5d9bd60368684fe52a704d46f9e7fc413918f18d046ec40e1"},
|
||||
{file = "coverage-7.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f39ae2f63f37472c17b4990f794035c9890418b1b8cca75c01193f3c8d3e01be"},
|
||||
{file = "coverage-7.11.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:7db53b5cdd2917b6eaadd0b1251cf4e7d96f4a8d24e174bdbdf2f65b5ea7994d"},
|
||||
{file = "coverage-7.11.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:10ad04ac3a122048688387828b4537bc9cf60c0bf4869c1e9989c46e45690b82"},
|
||||
{file = "coverage-7.11.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4036cc9c7983a2b1f2556d574d2eb2154ac6ed55114761685657e38782b23f52"},
|
||||
{file = "coverage-7.11.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:7ab934dd13b1c5e94b692b1e01bd87e4488cb746e3a50f798cb9464fd128374b"},
|
||||
{file = "coverage-7.11.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:59a6e5a265f7cfc05f76e3bb53eca2e0dfe90f05e07e849930fecd6abb8f40b4"},
|
||||
{file = "coverage-7.11.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:df01d6c4c81e15a7c88337b795bb7595a8596e92310266b5072c7e301168efbd"},
|
||||
{file = "coverage-7.11.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:8c934bd088eed6174210942761e38ee81d28c46de0132ebb1801dbe36a390dcc"},
|
||||
{file = "coverage-7.11.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5a03eaf7ec24078ad64a07f02e30060aaf22b91dedf31a6b24d0d98d2bba7f48"},
|
||||
{file = "coverage-7.11.0-cp313-cp313-win32.whl", hash = "sha256:695340f698a5f56f795b2836abe6fb576e7c53d48cd155ad2f80fd24bc63a040"},
|
||||
{file = "coverage-7.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:2727d47fce3ee2bac648528e41455d1b0c46395a087a229deac75e9f88ba5a05"},
|
||||
{file = "coverage-7.11.0-cp313-cp313-win_arm64.whl", hash = "sha256:0efa742f431529699712b92ecdf22de8ff198df41e43aeaaadf69973eb93f17a"},
|
||||
{file = "coverage-7.11.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:587c38849b853b157706407e9ebdca8fd12f45869edb56defbef2daa5fb0812b"},
|
||||
{file = "coverage-7.11.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b971bdefdd75096163dd4261c74be813c4508477e39ff7b92191dea19f24cd37"},
|
||||
{file = "coverage-7.11.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:269bfe913b7d5be12ab13a95f3a76da23cf147be7fa043933320ba5625f0a8de"},
|
||||
{file = "coverage-7.11.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:dadbcce51a10c07b7c72b0ce4a25e4b6dcb0c0372846afb8e5b6307a121eb99f"},
|
||||
{file = "coverage-7.11.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9ed43fa22c6436f7957df036331f8fe4efa7af132054e1844918866cd228af6c"},
|
||||
{file = "coverage-7.11.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9516add7256b6713ec08359b7b05aeff8850c98d357784c7205b2e60aa2513fa"},
|
||||
{file = "coverage-7.11.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:eb92e47c92fcbcdc692f428da67db33337fa213756f7adb6a011f7b5a7a20740"},
|
||||
{file = "coverage-7.11.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:d06f4fc7acf3cabd6d74941d53329e06bab00a8fe10e4df2714f0b134bfc64ef"},
|
||||
{file = "coverage-7.11.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:6fbcee1a8f056af07ecd344482f711f563a9eb1c2cad192e87df00338ec3cdb0"},
|
||||
{file = "coverage-7.11.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dbbf012be5f32533a490709ad597ad8a8ff80c582a95adc8d62af664e532f9ca"},
|
||||
{file = "coverage-7.11.0-cp313-cp313t-win32.whl", hash = "sha256:cee6291bb4fed184f1c2b663606a115c743df98a537c969c3c64b49989da96c2"},
|
||||
{file = "coverage-7.11.0-cp313-cp313t-win_amd64.whl", hash = "sha256:a386c1061bf98e7ea4758e4313c0ab5ecf57af341ef0f43a0bf26c2477b5c268"},
|
||||
{file = "coverage-7.11.0-cp313-cp313t-win_arm64.whl", hash = "sha256:f9ea02ef40bb83823b2b04964459d281688fe173e20643870bb5d2edf68bc836"},
|
||||
{file = "coverage-7.11.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c770885b28fb399aaf2a65bbd1c12bf6f307ffd112d6a76c5231a94276f0c497"},
|
||||
{file = "coverage-7.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a3d0e2087dba64c86a6b254f43e12d264b636a39e88c5cc0a01a7c71bcfdab7e"},
|
||||
{file = "coverage-7.11.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:73feb83bb41c32811973b8565f3705caf01d928d972b72042b44e97c71fd70d1"},
|
||||
{file = "coverage-7.11.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c6f31f281012235ad08f9a560976cc2fc9c95c17604ff3ab20120fe480169bca"},
|
||||
{file = "coverage-7.11.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9570ad567f880ef675673992222746a124b9595506826b210fbe0ce3f0499cd"},
|
||||
{file = "coverage-7.11.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:8badf70446042553a773547a61fecaa734b55dc738cacf20c56ab04b77425e43"},
|
||||
{file = "coverage-7.11.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a09c1211959903a479e389685b7feb8a17f59ec5a4ef9afde7650bd5eabc2777"},
|
||||
{file = "coverage-7.11.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:5ef83b107f50db3f9ae40f69e34b3bd9337456c5a7fe3461c7abf8b75dd666a2"},
|
||||
{file = "coverage-7.11.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:f91f927a3215b8907e214af77200250bb6aae36eca3f760f89780d13e495388d"},
|
||||
{file = "coverage-7.11.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:cdbcd376716d6b7fbfeedd687a6c4be019c5a5671b35f804ba76a4c0a778cba4"},
|
||||
{file = "coverage-7.11.0-cp314-cp314-win32.whl", hash = "sha256:bab7ec4bb501743edc63609320aaec8cd9188b396354f482f4de4d40a9d10721"},
|
||||
{file = "coverage-7.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:3d4ba9a449e9364a936a27322b20d32d8b166553bfe63059bd21527e681e2fad"},
|
||||
{file = "coverage-7.11.0-cp314-cp314-win_arm64.whl", hash = "sha256:ce37f215223af94ef0f75ac68ea096f9f8e8c8ec7d6e8c346ee45c0d363f0479"},
|
||||
{file = "coverage-7.11.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:f413ce6e07e0d0dc9c433228727b619871532674b45165abafe201f200cc215f"},
|
||||
{file = "coverage-7.11.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:05791e528a18f7072bf5998ba772fe29db4da1234c45c2087866b5ba4dea710e"},
|
||||
{file = "coverage-7.11.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cacb29f420cfeb9283b803263c3b9a068924474ff19ca126ba9103e1278dfa44"},
|
||||
{file = "coverage-7.11.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:314c24e700d7027ae3ab0d95fbf8d53544fca1f20345fd30cd219b737c6e58d3"},
|
||||
{file = "coverage-7.11.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:630d0bd7a293ad2fc8b4b94e5758c8b2536fdf36c05f1681270203e463cbfa9b"},
|
||||
{file = "coverage-7.11.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e89641f5175d65e2dbb44db15fe4ea48fade5d5bbb9868fdc2b4fce22f4a469d"},
|
||||
{file = "coverage-7.11.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c9f08ea03114a637dab06cedb2e914da9dc67fa52c6015c018ff43fdde25b9c2"},
|
||||
{file = "coverage-7.11.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ce9f3bde4e9b031eaf1eb61df95c1401427029ea1bfddb8621c1161dcb0fa02e"},
|
||||
{file = "coverage-7.11.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:e4dc07e95495923d6fd4d6c27bf70769425b71c89053083843fd78f378558996"},
|
||||
{file = "coverage-7.11.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:424538266794db2861db4922b05d729ade0940ee69dcf0591ce8f69784db0e11"},
|
||||
{file = "coverage-7.11.0-cp314-cp314t-win32.whl", hash = "sha256:4c1eeb3fb8eb9e0190bebafd0462936f75717687117339f708f395fe455acc73"},
|
||||
{file = "coverage-7.11.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b56efee146c98dbf2cf5cffc61b9829d1e94442df4d7398b26892a53992d3547"},
|
||||
{file = "coverage-7.11.0-cp314-cp314t-win_arm64.whl", hash = "sha256:b5c2705afa83f49bd91962a4094b6b082f94aef7626365ab3f8f4bd159c5acf3"},
|
||||
{file = "coverage-7.11.0-py3-none-any.whl", hash = "sha256:4b7589765348d78fb4e5fb6ea35d07564e387da2fc5efff62e0222971f155f68"},
|
||||
{file = "coverage-7.11.0.tar.gz", hash = "sha256:167bd504ac1ca2af7ff3b81d245dfea0292c5032ebef9d66cc08a7d28c1b8050"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
@@ -247,66 +251,66 @@ toml = ["tomli ; python_full_version <= \"3.11.0a6\""]
|
||||
|
||||
[[package]]
|
||||
name = "cryptography"
|
||||
version = "46.0.1"
|
||||
version = "46.0.3"
|
||||
description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
|
||||
optional = false
|
||||
python-versions = "!=3.9.0,!=3.9.1,>=3.8"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "cryptography-46.0.1-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:1cd6d50c1a8b79af1a6f703709d8973845f677c8e97b1268f5ff323d38ce8475"},
|
||||
{file = "cryptography-46.0.1-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0ff483716be32690c14636e54a1f6e2e1b7bf8e22ca50b989f88fa1b2d287080"},
|
||||
{file = "cryptography-46.0.1-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9873bf7c1f2a6330bdfe8621e7ce64b725784f9f0c3a6a55c3047af5849f920e"},
|
||||
{file = "cryptography-46.0.1-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:0dfb7c88d4462a0cfdd0d87a3c245a7bc3feb59de101f6ff88194f740f72eda6"},
|
||||
{file = "cryptography-46.0.1-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e22801b61613ebdebf7deb18b507919e107547a1d39a3b57f5f855032dd7cfb8"},
|
||||
{file = "cryptography-46.0.1-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:757af4f6341ce7a1e47c326ca2a81f41d236070217e5fbbad61bbfe299d55d28"},
|
||||
{file = "cryptography-46.0.1-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f7a24ea78de345cfa7f6a8d3bde8b242c7fac27f2bd78fa23474ca38dfaeeab9"},
|
||||
{file = "cryptography-46.0.1-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:9e8776dac9e660c22241b6587fae51a67b4b0147daa4d176b172c3ff768ad736"},
|
||||
{file = "cryptography-46.0.1-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9f40642a140c0c8649987027867242b801486865277cbabc8c6059ddef16dc8b"},
|
||||
{file = "cryptography-46.0.1-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:449ef2b321bec7d97ef2c944173275ebdab78f3abdd005400cc409e27cd159ab"},
|
||||
{file = "cryptography-46.0.1-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:2dd339ba3345b908fa3141ddba4025568fa6fd398eabce3ef72a29ac2d73ad75"},
|
||||
{file = "cryptography-46.0.1-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7411c910fb2a412053cf33cfad0153ee20d27e256c6c3f14d7d7d1d9fec59fd5"},
|
||||
{file = "cryptography-46.0.1-cp311-abi3-win32.whl", hash = "sha256:cbb8e769d4cac884bb28e3ff620ef1001b75588a5c83c9c9f1fdc9afbe7f29b0"},
|
||||
{file = "cryptography-46.0.1-cp311-abi3-win_amd64.whl", hash = "sha256:92e8cfe8bd7dd86eac0a677499894862cd5cc2fd74de917daa881d00871ac8e7"},
|
||||
{file = "cryptography-46.0.1-cp311-abi3-win_arm64.whl", hash = "sha256:db5597a4c7353b2e5fb05a8e6cb74b56a4658a2b7bf3cb6b1821ae7e7fd6eaa0"},
|
||||
{file = "cryptography-46.0.1-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:4c49eda9a23019e11d32a0eb51a27b3e7ddedde91e099c0ac6373e3aacc0d2ee"},
|
||||
{file = "cryptography-46.0.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9babb7818fdd71394e576cf26c5452df77a355eac1a27ddfa24096665a27f8fd"},
|
||||
{file = "cryptography-46.0.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9f2c4cc63be3ef43c0221861177cee5d14b505cd4d4599a89e2cd273c4d3542a"},
|
||||
{file = "cryptography-46.0.1-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:41c281a74df173876da1dc9a9b6953d387f06e3d3ed9284e3baae3ab3f40883a"},
|
||||
{file = "cryptography-46.0.1-cp314-cp314t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0a17377fa52563d730248ba1f68185461fff36e8bc75d8787a7dd2e20a802b7a"},
|
||||
{file = "cryptography-46.0.1-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:0d1922d9280e08cde90b518a10cd66831f632960a8d08cb3418922d83fce6f12"},
|
||||
{file = "cryptography-46.0.1-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:af84e8e99f1a82cea149e253014ea9dc89f75b82c87bb6c7242203186f465129"},
|
||||
{file = "cryptography-46.0.1-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:ef648d2c690703501714588b2ba640facd50fd16548133b11b2859e8655a69da"},
|
||||
{file = "cryptography-46.0.1-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:e94eb5fa32a8a9f9bf991f424f002913e3dd7c699ef552db9b14ba6a76a6313b"},
|
||||
{file = "cryptography-46.0.1-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:534b96c0831855e29fc3b069b085fd185aa5353033631a585d5cd4dd5d40d657"},
|
||||
{file = "cryptography-46.0.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f9b55038b5c6c47559aa33626d8ecd092f354e23de3c6975e4bb205df128a2a0"},
|
||||
{file = "cryptography-46.0.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ec13b7105117dbc9afd023300fb9954d72ca855c274fe563e72428ece10191c0"},
|
||||
{file = "cryptography-46.0.1-cp314-cp314t-win32.whl", hash = "sha256:504e464944f2c003a0785b81668fe23c06f3b037e9cb9f68a7c672246319f277"},
|
||||
{file = "cryptography-46.0.1-cp314-cp314t-win_amd64.whl", hash = "sha256:c52fded6383f7e20eaf70a60aeddd796b3677c3ad2922c801be330db62778e05"},
|
||||
{file = "cryptography-46.0.1-cp314-cp314t-win_arm64.whl", hash = "sha256:9495d78f52c804b5ec8878b5b8c7873aa8e63db9cd9ee387ff2db3fffe4df784"},
|
||||
{file = "cryptography-46.0.1-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:d84c40bdb8674c29fa192373498b6cb1e84f882889d21a471b45d1f868d8d44b"},
|
||||
{file = "cryptography-46.0.1-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9ed64e5083fa806709e74fc5ea067dfef9090e5b7a2320a49be3c9df3583a2d8"},
|
||||
{file = "cryptography-46.0.1-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:341fb7a26bc9d6093c1b124b9f13acc283d2d51da440b98b55ab3f79f2522ead"},
|
||||
{file = "cryptography-46.0.1-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6ef1488967e729948d424d09c94753d0167ce59afba8d0f6c07a22b629c557b2"},
|
||||
{file = "cryptography-46.0.1-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7823bc7cdf0b747ecfb096d004cc41573c2f5c7e3a29861603a2871b43d3ef32"},
|
||||
{file = "cryptography-46.0.1-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:f736ab8036796f5a119ff8211deda416f8c15ce03776db704a7a4e17381cb2ef"},
|
||||
{file = "cryptography-46.0.1-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:e46710a240a41d594953012213ea8ca398cd2448fbc5d0f1be8160b5511104a0"},
|
||||
{file = "cryptography-46.0.1-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:84ef1f145de5aee82ea2447224dc23f065ff4cc5791bb3b506615957a6ba8128"},
|
||||
{file = "cryptography-46.0.1-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:9394c7d5a7565ac5f7d9ba38b2617448eba384d7b107b262d63890079fad77ca"},
|
||||
{file = "cryptography-46.0.1-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:ed957044e368ed295257ae3d212b95456bd9756df490e1ac4538857f67531fcc"},
|
||||
{file = "cryptography-46.0.1-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:f7de12fa0eee6234de9a9ce0ffcfa6ce97361db7a50b09b65c63ac58e5f22fc7"},
|
||||
{file = "cryptography-46.0.1-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7fab1187b6c6b2f11a326f33b036f7168f5b996aedd0c059f9738915e4e8f53a"},
|
||||
{file = "cryptography-46.0.1-cp38-abi3-win32.whl", hash = "sha256:45f790934ac1018adeba46a0f7289b2b8fe76ba774a88c7f1922213a56c98bc1"},
|
||||
{file = "cryptography-46.0.1-cp38-abi3-win_amd64.whl", hash = "sha256:7176a5ab56fac98d706921f6416a05e5aff7df0e4b91516f450f8627cda22af3"},
|
||||
{file = "cryptography-46.0.1-cp38-abi3-win_arm64.whl", hash = "sha256:efc9e51c3e595267ff84adf56e9b357db89ab2279d7e375ffcaf8f678606f3d9"},
|
||||
{file = "cryptography-46.0.1-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:fd4b5e2ee4e60425711ec65c33add4e7a626adef79d66f62ba0acfd493af282d"},
|
||||
{file = "cryptography-46.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:48948940d0ae00483e85e9154bb42997d0b77c21e43a77b7773c8c80de532ac5"},
|
||||
{file = "cryptography-46.0.1-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:b9c79af2c3058430d911ff1a5b2b96bbfe8da47d5ed961639ce4681886614e70"},
|
||||
{file = "cryptography-46.0.1-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:0ca4be2af48c24df689a150d9cd37404f689e2968e247b6b8ff09bff5bcd786f"},
|
||||
{file = "cryptography-46.0.1-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:13e67c4d3fb8b6bc4ef778a7ccdd8df4cd15b4bcc18f4239c8440891a11245cc"},
|
||||
{file = "cryptography-46.0.1-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:15b5fd9358803b0d1cc42505a18d8bca81dabb35b5cfbfea1505092e13a9d96d"},
|
||||
{file = "cryptography-46.0.1-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:e34da95e29daf8a71cb2841fd55df0511539a6cdf33e6f77c1e95e44006b9b46"},
|
||||
{file = "cryptography-46.0.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:34f04b7311174469ab3ac2647469743720f8b6c8b046f238e5cb27905695eb2a"},
|
||||
{file = "cryptography-46.0.1.tar.gz", hash = "sha256:ed570874e88f213437f5cf758f9ef26cbfc3f336d889b1e592ee11283bb8d1c7"},
|
||||
{file = "cryptography-46.0.3-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:109d4ddfadf17e8e7779c39f9b18111a09efb969a301a31e987416a0191ed93a"},
|
||||
{file = "cryptography-46.0.3-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:09859af8466b69bc3c27bdf4f5d84a665e0f7ab5088412e9e2ec49758eca5cbc"},
|
||||
{file = "cryptography-46.0.3-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:01ca9ff2885f3acc98c29f1860552e37f6d7c7d013d7334ff2a9de43a449315d"},
|
||||
{file = "cryptography-46.0.3-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6eae65d4c3d33da080cff9c4ab1f711b15c1d9760809dad6ea763f3812d254cb"},
|
||||
{file = "cryptography-46.0.3-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5bf0ed4490068a2e72ac03d786693adeb909981cc596425d09032d372bcc849"},
|
||||
{file = "cryptography-46.0.3-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5ecfccd2329e37e9b7112a888e76d9feca2347f12f37918facbb893d7bb88ee8"},
|
||||
{file = "cryptography-46.0.3-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a2c0cd47381a3229c403062f764160d57d4d175e022c1df84e168c6251a22eec"},
|
||||
{file = "cryptography-46.0.3-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:549e234ff32571b1f4076ac269fcce7a808d3bf98b76c8dd560e42dbc66d7d91"},
|
||||
{file = "cryptography-46.0.3-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:c0a7bb1a68a5d3471880e264621346c48665b3bf1c3759d682fc0864c540bd9e"},
|
||||
{file = "cryptography-46.0.3-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:10b01676fc208c3e6feeb25a8b83d81767e8059e1fe86e1dc62d10a3018fa926"},
|
||||
{file = "cryptography-46.0.3-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0abf1ffd6e57c67e92af68330d05760b7b7efb243aab8377e583284dbab72c71"},
|
||||
{file = "cryptography-46.0.3-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a04bee9ab6a4da801eb9b51f1b708a1b5b5c9eb48c03f74198464c66f0d344ac"},
|
||||
{file = "cryptography-46.0.3-cp311-abi3-win32.whl", hash = "sha256:f260d0d41e9b4da1ed1e0f1ce571f97fe370b152ab18778e9e8f67d6af432018"},
|
||||
{file = "cryptography-46.0.3-cp311-abi3-win_amd64.whl", hash = "sha256:a9a3008438615669153eb86b26b61e09993921ebdd75385ddd748702c5adfddb"},
|
||||
{file = "cryptography-46.0.3-cp311-abi3-win_arm64.whl", hash = "sha256:5d7f93296ee28f68447397bf5198428c9aeeab45705a55d53a6343455dcb2c3c"},
|
||||
{file = "cryptography-46.0.3-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:00a5e7e87938e5ff9ff5447ab086a5706a957137e6e433841e9d24f38a065217"},
|
||||
{file = "cryptography-46.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c8daeb2d2174beb4575b77482320303f3d39b8e81153da4f0fb08eb5fe86a6c5"},
|
||||
{file = "cryptography-46.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:39b6755623145ad5eff1dab323f4eae2a32a77a7abef2c5089a04a3d04366715"},
|
||||
{file = "cryptography-46.0.3-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:db391fa7c66df6762ee3f00c95a89e6d428f4d60e7abc8328f4fe155b5ac6e54"},
|
||||
{file = "cryptography-46.0.3-cp314-cp314t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:78a97cf6a8839a48c49271cdcbd5cf37ca2c1d6b7fdd86cc864f302b5e9bf459"},
|
||||
{file = "cryptography-46.0.3-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:dfb781ff7eaa91a6f7fd41776ec37c5853c795d3b358d4896fdbb5df168af422"},
|
||||
{file = "cryptography-46.0.3-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:6f61efb26e76c45c4a227835ddeae96d83624fb0d29eb5df5b96e14ed1a0afb7"},
|
||||
{file = "cryptography-46.0.3-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:23b1a8f26e43f47ceb6d6a43115f33a5a37d57df4ea0ca295b780ae8546e8044"},
|
||||
{file = "cryptography-46.0.3-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:b419ae593c86b87014b9be7396b385491ad7f320bde96826d0dd174459e54665"},
|
||||
{file = "cryptography-46.0.3-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:50fc3343ac490c6b08c0cf0d704e881d0d660be923fd3076db3e932007e726e3"},
|
||||
{file = "cryptography-46.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:22d7e97932f511d6b0b04f2bfd818d73dcd5928db509460aaf48384778eb6d20"},
|
||||
{file = "cryptography-46.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d55f3dffadd674514ad19451161118fd010988540cee43d8bc20675e775925de"},
|
||||
{file = "cryptography-46.0.3-cp314-cp314t-win32.whl", hash = "sha256:8a6e050cb6164d3f830453754094c086ff2d0b2f3a897a1d9820f6139a1f0914"},
|
||||
{file = "cryptography-46.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:760f83faa07f8b64e9c33fc963d790a2edb24efb479e3520c14a45741cd9b2db"},
|
||||
{file = "cryptography-46.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:516ea134e703e9fe26bcd1277a4b59ad30586ea90c365a87781d7887a646fe21"},
|
||||
{file = "cryptography-46.0.3-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:cb3d760a6117f621261d662bccc8ef5bc32ca673e037c83fbe565324f5c46936"},
|
||||
{file = "cryptography-46.0.3-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4b7387121ac7d15e550f5cb4a43aef2559ed759c35df7336c402bb8275ac9683"},
|
||||
{file = "cryptography-46.0.3-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:15ab9b093e8f09daab0f2159bb7e47532596075139dd74365da52ecc9cb46c5d"},
|
||||
{file = "cryptography-46.0.3-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:46acf53b40ea38f9c6c229599a4a13f0d46a6c3fa9ef19fc1a124d62e338dfa0"},
|
||||
{file = "cryptography-46.0.3-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10ca84c4668d066a9878890047f03546f3ae0a6b8b39b697457b7757aaf18dbc"},
|
||||
{file = "cryptography-46.0.3-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:36e627112085bb3b81b19fed209c05ce2a52ee8b15d161b7c643a7d5a88491f3"},
|
||||
{file = "cryptography-46.0.3-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:1000713389b75c449a6e979ffc7dcc8ac90b437048766cef052d4d30b8220971"},
|
||||
{file = "cryptography-46.0.3-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:b02cf04496f6576afffef5ddd04a0cb7d49cf6be16a9059d793a30b035f6b6ac"},
|
||||
{file = "cryptography-46.0.3-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:71e842ec9bc7abf543b47cf86b9a743baa95f4677d22baa4c7d5c69e49e9bc04"},
|
||||
{file = "cryptography-46.0.3-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:402b58fc32614f00980b66d6e56a5b4118e6cb362ae8f3fda141ba4689bd4506"},
|
||||
{file = "cryptography-46.0.3-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ef639cb3372f69ec44915fafcd6698b6cc78fbe0c2ea41be867f6ed612811963"},
|
||||
{file = "cryptography-46.0.3-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3b51b8ca4f1c6453d8829e1eb7299499ca7f313900dd4d89a24b8b87c0a780d4"},
|
||||
{file = "cryptography-46.0.3-cp38-abi3-win32.whl", hash = "sha256:6276eb85ef938dc035d59b87c8a7dc559a232f954962520137529d77b18ff1df"},
|
||||
{file = "cryptography-46.0.3-cp38-abi3-win_amd64.whl", hash = "sha256:416260257577718c05135c55958b674000baef9a1c7d9e8f306ec60d71db850f"},
|
||||
{file = "cryptography-46.0.3-cp38-abi3-win_arm64.whl", hash = "sha256:d89c3468de4cdc4f08a57e214384d0471911a3830fcdaf7a8cc587e42a866372"},
|
||||
{file = "cryptography-46.0.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:a23582810fedb8c0bc47524558fb6c56aac3fc252cb306072fd2815da2a47c32"},
|
||||
{file = "cryptography-46.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:e7aec276d68421f9574040c26e2a7c3771060bc0cff408bae1dcb19d3ab1e63c"},
|
||||
{file = "cryptography-46.0.3-pp311-pypy311_pp73-macosx_10_9_x86_64.whl", hash = "sha256:7ce938a99998ed3c8aa7e7272dca1a610401ede816d36d0693907d863b10d9ea"},
|
||||
{file = "cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:191bb60a7be5e6f54e30ba16fdfae78ad3a342a0599eb4193ba88e3f3d6e185b"},
|
||||
{file = "cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c70cc23f12726be8f8bc72e41d5065d77e4515efae3690326764ea1b07845cfb"},
|
||||
{file = "cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:9394673a9f4de09e28b5356e7fff97d778f8abad85c9d5ac4a4b7e25a0de7717"},
|
||||
{file = "cryptography-46.0.3-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:94cd0549accc38d1494e1f8de71eca837d0509d0d44bf11d158524b0e12cebf9"},
|
||||
{file = "cryptography-46.0.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:6b5063083824e5509fdba180721d55909ffacccc8adbec85268b48439423d78c"},
|
||||
{file = "cryptography-46.0.3.tar.gz", hash = "sha256:a8b17438104fed022ce745b362294d9ce35b4c2e45c1d958ad4a4b019285f4a1"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -319,7 +323,7 @@ nox = ["nox[uv] (>=2024.4.15)"]
|
||||
pep8test = ["check-sdist", "click (>=8.0.1)", "mypy (>=1.14)", "ruff (>=0.11.11)"]
|
||||
sdist = ["build (>=1.0.0)"]
|
||||
ssh = ["bcrypt (>=3.1.5)"]
|
||||
test = ["certifi (>=2024)", "cryptography-vectors (==46.0.1)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"]
|
||||
test = ["certifi (>=2024)", "cryptography-vectors (==46.0.3)", "pretend (>=0.7)", "pytest (>=7.4.0)", "pytest-benchmark (>=4.0)", "pytest-cov (>=2.10.1)", "pytest-xdist (>=3.5.0)"]
|
||||
test-randomorder = ["pytest-randomly"]
|
||||
|
||||
[[package]]
|
||||
@@ -408,15 +412,15 @@ zstd = ["zstandard (>=0.18.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "3.10"
|
||||
version = "3.11"
|
||||
description = "Internationalized Domain Names in Applications (IDNA)"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
markers = "platform_system != \"Pyodide\""
|
||||
files = [
|
||||
{file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"},
|
||||
{file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"},
|
||||
{file = "idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea"},
|
||||
{file = "idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
@@ -424,14 +428,14 @@ all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2
|
||||
|
||||
[[package]]
|
||||
name = "iniconfig"
|
||||
version = "2.1.0"
|
||||
version = "2.3.0"
|
||||
description = "brain-dead simple config-ini parsing"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
python-versions = ">=3.10"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760"},
|
||||
{file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"},
|
||||
{file = "iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12"},
|
||||
{file = "iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -619,14 +623,14 @@ six = ">=1.9.0"
|
||||
|
||||
[[package]]
|
||||
name = "rns"
|
||||
version = "1.0.0"
|
||||
version = "1.0.1"
|
||||
description = "Self-configuring, encrypted and resilient mesh networking stack for LoRa, packet radio, WiFi and everything in between"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "rns-1.0.0-py3-none-any.whl", hash = "sha256:5a9f18840510b69f89c6706d130177e2843c9e19c774707ae2661030d693dfc1"},
|
||||
{file = "rns-1.0.0.tar.gz", hash = "sha256:9f1c594e4eabd64dea4c1bd59ad1b9291e6a28b1d8ab5689a19708f13100735b"},
|
||||
{file = "rns-1.0.1-py3-none-any.whl", hash = "sha256:aa77b4c8e1b6899117666e1e55b05b3250416ab5fea2826254358ae320e8b3ed"},
|
||||
{file = "rns-1.0.1.tar.gz", hash = "sha256:f45ea52b065be09b8e2257425b6fcde1a49899ea41aee349936d182aa1844b26"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
@@ -635,30 +639,31 @@ pyserial = ">=3.5"
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.11.13"
|
||||
version = "0.14.3"
|
||||
description = "An extremely fast Python linter and code formatter, written in Rust."
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "ruff-0.11.13-py3-none-linux_armv6l.whl", hash = "sha256:4bdfbf1240533f40042ec00c9e09a3aade6f8c10b6414cf11b519488d2635d46"},
|
||||
{file = "ruff-0.11.13-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:aef9c9ed1b5ca28bb15c7eac83b8670cf3b20b478195bd49c8d756ba0a36cf48"},
|
||||
{file = "ruff-0.11.13-py3-none-macosx_11_0_arm64.whl", hash = "sha256:53b15a9dfdce029c842e9a5aebc3855e9ab7771395979ff85b7c1dedb53ddc2b"},
|
||||
{file = "ruff-0.11.13-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab153241400789138d13f362c43f7edecc0edfffce2afa6a68434000ecd8f69a"},
|
||||
{file = "ruff-0.11.13-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6c51f93029d54a910d3d24f7dd0bb909e31b6cd989a5e4ac513f4eb41629f0dc"},
|
||||
{file = "ruff-0.11.13-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1808b3ed53e1a777c2ef733aca9051dc9bf7c99b26ece15cb59a0320fbdbd629"},
|
||||
{file = "ruff-0.11.13-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:d28ce58b5ecf0f43c1b71edffabe6ed7f245d5336b17805803312ec9bc665933"},
|
||||
{file = "ruff-0.11.13-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:55e4bc3a77842da33c16d55b32c6cac1ec5fb0fbec9c8c513bdce76c4f922165"},
|
||||
{file = "ruff-0.11.13-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:633bf2c6f35678c56ec73189ba6fa19ff1c5e4807a78bf60ef487b9dd272cc71"},
|
||||
{file = "ruff-0.11.13-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ffbc82d70424b275b089166310448051afdc6e914fdab90e08df66c43bb5ca9"},
|
||||
{file = "ruff-0.11.13-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4a9ddd3ec62a9a89578c85842b836e4ac832d4a2e0bfaad3b02243f930ceafcc"},
|
||||
{file = "ruff-0.11.13-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d237a496e0778d719efb05058c64d28b757c77824e04ffe8796c7436e26712b7"},
|
||||
{file = "ruff-0.11.13-py3-none-musllinux_1_2_i686.whl", hash = "sha256:26816a218ca6ef02142343fd24c70f7cd8c5aa6c203bca284407adf675984432"},
|
||||
{file = "ruff-0.11.13-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:51c3f95abd9331dc5b87c47ac7f376db5616041173826dfd556cfe3d4977f492"},
|
||||
{file = "ruff-0.11.13-py3-none-win32.whl", hash = "sha256:96c27935418e4e8e77a26bb05962817f28b8ef3843a6c6cc49d8783b5507f250"},
|
||||
{file = "ruff-0.11.13-py3-none-win_amd64.whl", hash = "sha256:29c3189895a8a6a657b7af4e97d330c8a3afd2c9c8f46c81e2fc5a31866517e3"},
|
||||
{file = "ruff-0.11.13-py3-none-win_arm64.whl", hash = "sha256:b4385285e9179d608ff1d2fb9922062663c658605819a6876d8beef0c30b7f3b"},
|
||||
{file = "ruff-0.11.13.tar.gz", hash = "sha256:26fa247dc68d1d4e72c179e08889a25ac0c7ba4d78aecfc835d49cbfd60bf514"},
|
||||
{file = "ruff-0.14.3-py3-none-linux_armv6l.whl", hash = "sha256:876b21e6c824f519446715c1342b8e60f97f93264012de9d8d10314f8a79c371"},
|
||||
{file = "ruff-0.14.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b6fd8c79b457bedd2abf2702b9b472147cd860ed7855c73a5247fa55c9117654"},
|
||||
{file = "ruff-0.14.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:71ff6edca490c308f083156938c0c1a66907151263c4abdcb588602c6e696a14"},
|
||||
{file = "ruff-0.14.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:786ee3ce6139772ff9272aaf43296d975c0217ee1b97538a98171bf0d21f87ed"},
|
||||
{file = "ruff-0.14.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:cd6291d0061811c52b8e392f946889916757610d45d004e41140d81fb6cd5ddc"},
|
||||
{file = "ruff-0.14.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a497ec0c3d2c88561b6d90f9c29f5ae68221ac00d471f306fa21fa4264ce5fcd"},
|
||||
{file = "ruff-0.14.3-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:e231e1be58fc568950a04fbe6887c8e4b85310e7889727e2b81db205c45059eb"},
|
||||
{file = "ruff-0.14.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:469e35872a09c0e45fecf48dd960bfbce056b5db2d5e6b50eca329b4f853ae20"},
|
||||
{file = "ruff-0.14.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d6bc90307c469cb9d28b7cfad90aaa600b10d67c6e22026869f585e1e8a2db0"},
|
||||
{file = "ruff-0.14.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2f8a0bbcffcfd895df39c9a4ecd59bb80dca03dc43f7fb63e647ed176b741e"},
|
||||
{file = "ruff-0.14.3-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:678fdd7c7d2d94851597c23ee6336d25f9930b460b55f8598e011b57c74fd8c5"},
|
||||
{file = "ruff-0.14.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:1ec1ac071e7e37e0221d2f2dbaf90897a988c531a8592a6a5959f0603a1ecf5e"},
|
||||
{file = "ruff-0.14.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:afcdc4b5335ef440d19e7df9e8ae2ad9f749352190e96d481dc501b753f0733e"},
|
||||
{file = "ruff-0.14.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:7bfc42f81862749a7136267a343990f865e71fe2f99cf8d2958f684d23ce3dfa"},
|
||||
{file = "ruff-0.14.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a65e448cfd7e9c59fae8cf37f9221585d3354febaad9a07f29158af1528e165f"},
|
||||
{file = "ruff-0.14.3-py3-none-win32.whl", hash = "sha256:f3d91857d023ba93e14ed2d462ab62c3428f9bbf2b4fbac50a03ca66d31991f7"},
|
||||
{file = "ruff-0.14.3-py3-none-win_amd64.whl", hash = "sha256:d7b7006ac0756306db212fd37116cce2bd307e1e109375e1c6c106002df0ae5f"},
|
||||
{file = "ruff-0.14.3-py3-none-win_arm64.whl", hash = "sha256:26eb477ede6d399d898791d01961e16b86f02bc2486d0d1a7a9bb2379d055dc1"},
|
||||
{file = "ruff-0.14.3.tar.gz", hash = "sha256:4ff876d2ab2b161b6de0aa1f5bd714e8e9b4033dc122ee006925fbacc4f62153"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -689,4 +694,4 @@ files = [
|
||||
[metadata]
|
||||
lock-version = "2.1"
|
||||
python-versions = ">=3.13"
|
||||
content-hash = "1164f4cb57e282bd41d46df9cdbb43e0756ee0269442235c80aa96df57f740dc"
|
||||
content-hash = "272b66fa2a425d4b1b5dfe2640b2386bf57c447712d60886ed7627ffafd87540"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "ren-browser"
|
||||
version = "0.2.0"
|
||||
version = "0.2.2"
|
||||
description = "A browser for the Reticulum Network."
|
||||
authors = [
|
||||
{name = "Sudo-Ivan"}
|
||||
@@ -9,28 +9,32 @@ module = "ren_browser.app"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.13"
|
||||
dependencies = [
|
||||
"flet (>=0.28.3,<0.29.0)",
|
||||
"flet[all] (>=0.28.3,<0.29.0)",
|
||||
"rns (>=1.0.0,<1.5.0)"
|
||||
]
|
||||
|
||||
[build-system]
|
||||
requires = ["poetry-core>=2.0.0,<3.0.0"]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
packages = ["ren_browser"]
|
||||
|
||||
[project.scripts]
|
||||
ren-browser = "ren_browser.app:run"
|
||||
ren-browser-web = "ren_browser.app:web"
|
||||
ren-browser-android = "ren_browser.app:android"
|
||||
ren-browser-ios = "ren_browser.app:ios"
|
||||
ren-browser-dev = "ren_browser.app:run_dev"
|
||||
ren-browser-web-dev = "ren_browser.app:web_dev"
|
||||
ren-browser-android-dev = "ren_browser.app:android_dev"
|
||||
ren-browser-ios-dev = "ren_browser.app:ios_dev"
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
ruff = "^0.11.11"
|
||||
pytest = "^8.4.2"
|
||||
pytest-cov = "^7.0.0"
|
||||
pytest-mock = "^3.15.1"
|
||||
pytest-asyncio = "^1.2.0"
|
||||
[dependency-groups]
|
||||
dev = [
|
||||
"ruff>=0.11.11,<1.0.0",
|
||||
"pytest>=8.4.2,<9.0.0",
|
||||
"pytest-cov>=7.0.0,<8.0.0",
|
||||
"pytest-mock>=3.15.1,<4.0.0",
|
||||
"pytest-asyncio>=1.2.0,<2.0.0"
|
||||
]
|
||||
|
||||
[tool.flet.flutter.pubspec.dependency_overrides]
|
||||
webview_flutter_android = "4.10.1"
|
||||
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
This module provides services for listening to and collecting network
|
||||
announces from the Reticulum network.
|
||||
"""
|
||||
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
|
||||
import RNS
|
||||
|
||||
|
||||
|
||||
@dataclass
|
||||
class Announce:
|
||||
"""Represents a Reticulum network announce.
|
||||
@@ -21,6 +21,7 @@ class Announce:
|
||||
display_name: str | None
|
||||
timestamp: int
|
||||
|
||||
|
||||
class AnnounceService:
|
||||
"""Service to listen for Reticulum announces and collect them.
|
||||
|
||||
@@ -60,7 +61,11 @@ class AnnounceService:
|
||||
except UnicodeDecodeError:
|
||||
pass
|
||||
announce = Announce(destination_hash.hex(), display_name, ts)
|
||||
self.announces = [ann for ann in self.announces if ann.destination_hash != announce.destination_hash]
|
||||
self.announces = [
|
||||
ann
|
||||
for ann in self.announces
|
||||
if ann.destination_hash != announce.destination_hash
|
||||
]
|
||||
self.announces.insert(0, announce)
|
||||
if self.update_callback:
|
||||
self.update_callback(self.announces)
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
This module provides the entry point and platform-specific launchers for the
|
||||
Ren Browser, a browser for the Reticulum Network built with Flet.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
|
||||
import flet as ft
|
||||
@@ -15,36 +16,68 @@ from ren_browser.ui.ui import build_ui
|
||||
RENDERER = "plaintext"
|
||||
RNS_CONFIG_DIR = None
|
||||
|
||||
|
||||
async def main(page: Page):
|
||||
"""Initialize and launch the Ren Browser application.
|
||||
|
||||
Sets up the loading screen, initializes Reticulum network,
|
||||
and builds the main UI.
|
||||
"""
|
||||
page.title = "Ren Browser"
|
||||
page.theme_mode = ft.ThemeMode.DARK
|
||||
|
||||
loader = ft.Container(
|
||||
expand=True,
|
||||
alignment=ft.alignment.center,
|
||||
bgcolor=ft.Colors.SURFACE,
|
||||
content=ft.Column(
|
||||
[ft.ProgressRing(), ft.Text("Initializing reticulum network")],
|
||||
[
|
||||
ft.ProgressRing(color=ft.Colors.PRIMARY, width=50, height=50),
|
||||
ft.Container(height=20),
|
||||
ft.Text(
|
||||
"Initializing Reticulum Network...",
|
||||
size=16,
|
||||
color=ft.Colors.ON_SURFACE,
|
||||
text_align=ft.TextAlign.CENTER,
|
||||
),
|
||||
],
|
||||
alignment=ft.MainAxisAlignment.CENTER,
|
||||
horizontal_alignment=ft.CrossAxisAlignment.CENTER,
|
||||
spacing=10,
|
||||
),
|
||||
)
|
||||
page.add(loader)
|
||||
page.update()
|
||||
|
||||
def init_ret():
|
||||
import time
|
||||
|
||||
time.sleep(0.5)
|
||||
|
||||
# Initialize storage system
|
||||
storage = initialize_storage(page)
|
||||
|
||||
# Get Reticulum config directory
|
||||
if RNS_CONFIG_DIR:
|
||||
config_dir = RNS_CONFIG_DIR
|
||||
else:
|
||||
config_dir = storage.get_reticulum_config_path()
|
||||
# Get Reticulum config directory from storage manager
|
||||
config_dir = storage.get_reticulum_config_path()
|
||||
|
||||
# Update the global RNS_CONFIG_DIR so RNS uses the right path
|
||||
global RNS_CONFIG_DIR
|
||||
RNS_CONFIG_DIR = str(config_dir)
|
||||
|
||||
# Ensure any saved config is written to filesystem before RNS init
|
||||
try:
|
||||
saved_config = storage.load_config()
|
||||
if saved_config and saved_config.strip():
|
||||
config_file_path = config_dir / "config"
|
||||
config_file_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
config_file_path.write_text(saved_config, encoding="utf-8")
|
||||
except Exception as e:
|
||||
print(f"Warning: Failed to write config file: {e}")
|
||||
|
||||
try:
|
||||
# Set up logging capture first, before RNS init
|
||||
import ren_browser.logs
|
||||
|
||||
ren_browser.logs.setup_rns_logging()
|
||||
RNS.Reticulum(str(config_dir))
|
||||
except (OSError, ValueError):
|
||||
@@ -55,14 +88,31 @@ async def main(page: Page):
|
||||
|
||||
page.run_thread(init_ret)
|
||||
|
||||
|
||||
def run():
|
||||
"""Run Ren Browser with command line argument parsing."""
|
||||
global RENDERER, RNS_CONFIG_DIR
|
||||
parser = argparse.ArgumentParser(description="Ren Browser")
|
||||
parser.add_argument("-r", "--renderer", choices=["plaintext", "micron"], default=RENDERER, help="Select renderer (plaintext or micron)")
|
||||
parser.add_argument("-w", "--web", action="store_true", help="Launch in web browser mode")
|
||||
parser.add_argument("-p", "--port", type=int, default=None, help="Port for web server")
|
||||
parser.add_argument("-c", "--config-dir", type=str, default=None, help="RNS config directory (default: ~/.reticulum/)")
|
||||
parser.add_argument(
|
||||
"-r",
|
||||
"--renderer",
|
||||
choices=["plaintext", "micron"],
|
||||
default=RENDERER,
|
||||
help="Select renderer (plaintext or micron)",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-w", "--web", action="store_true", help="Launch in web browser mode"
|
||||
)
|
||||
parser.add_argument(
|
||||
"-p", "--port", type=int, default=None, help="Port for web server"
|
||||
)
|
||||
parser.add_argument(
|
||||
"-c",
|
||||
"--config-dir",
|
||||
type=str,
|
||||
default=None,
|
||||
help="RNS config directory (default: ~/.reticulum/)",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
RENDERER = args.renderer
|
||||
|
||||
@@ -71,6 +121,7 @@ def run():
|
||||
RNS_CONFIG_DIR = args.config_dir
|
||||
else:
|
||||
import pathlib
|
||||
|
||||
RNS_CONFIG_DIR = str(pathlib.Path.home() / ".reticulum")
|
||||
|
||||
if args.web:
|
||||
@@ -81,33 +132,41 @@ def run():
|
||||
else:
|
||||
ft.app(main)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run()
|
||||
|
||||
|
||||
def web():
|
||||
"""Launch Ren Browser in web mode."""
|
||||
ft.app(main, view=AppView.WEB_BROWSER)
|
||||
|
||||
|
||||
def android():
|
||||
"""Launch Ren Browser in Android mode."""
|
||||
ft.app(main, view=AppView.FLET_APP_WEB)
|
||||
|
||||
|
||||
def ios():
|
||||
"""Launch Ren Browser in iOS mode."""
|
||||
ft.app(main, view=AppView.FLET_APP_WEB)
|
||||
|
||||
|
||||
def run_dev():
|
||||
"""Launch Ren Browser in desktop mode."""
|
||||
ft.app(main)
|
||||
|
||||
|
||||
def web_dev():
|
||||
"""Launch Ren Browser in web mode."""
|
||||
ft.app(main, view=AppView.WEB_BROWSER)
|
||||
|
||||
|
||||
def android_dev():
|
||||
"""Launch Ren Browser in Android mode."""
|
||||
ft.app(main, view=AppView.FLET_APP_WEB)
|
||||
|
||||
|
||||
def ios_dev():
|
||||
"""Launch Ren Browser in iOS mode."""
|
||||
ft.app(main, view=AppView.FLET_APP_WEB)
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
Provides keyboard event handling and delegation to tab manager
|
||||
and UI components.
|
||||
"""
|
||||
|
||||
import flet as ft
|
||||
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
Provides centralized logging for application events, errors, and
|
||||
Reticulum network activities.
|
||||
"""
|
||||
|
||||
import datetime
|
||||
|
||||
import RNS
|
||||
@@ -12,6 +13,7 @@ ERROR_LOGS: list[str] = []
|
||||
RET_LOGS: list[str] = []
|
||||
_original_rns_log = RNS.log
|
||||
|
||||
|
||||
def log_ret(msg, *args, **kwargs):
|
||||
"""Log Reticulum messages with timestamp.
|
||||
|
||||
@@ -25,14 +27,16 @@ def log_ret(msg, *args, **kwargs):
|
||||
RET_LOGS.append(f"[{timestamp}] {msg}")
|
||||
return _original_rns_log(msg, *args, **kwargs)
|
||||
|
||||
|
||||
def setup_rns_logging():
|
||||
"""Set up RNS log replacement. Call this after RNS.Reticulum initialization."""
|
||||
global _original_rns_log
|
||||
# Only set up if not already done and if RNS.log is not already our function
|
||||
if RNS.log != log_ret and _original_rns_log != log_ret:
|
||||
if RNS.log is not log_ret and _original_rns_log is not log_ret:
|
||||
_original_rns_log = RNS.log
|
||||
RNS.log = log_ret
|
||||
|
||||
|
||||
def log_error(msg: str):
|
||||
"""Log error messages to both error and application logs.
|
||||
|
||||
@@ -44,6 +48,7 @@ def log_error(msg: str):
|
||||
ERROR_LOGS.append(f"[{timestamp}] {msg}")
|
||||
APP_LOGS.append(f"[{timestamp}] ERROR: {msg}")
|
||||
|
||||
|
||||
def log_app(msg: str):
|
||||
"""Log application messages.
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
Handles downloading pages from the Reticulum network using
|
||||
the nomadnetwork protocol.
|
||||
"""
|
||||
|
||||
import threading
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
@@ -10,7 +11,6 @@ from dataclasses import dataclass
|
||||
import RNS
|
||||
|
||||
|
||||
|
||||
@dataclass
|
||||
class PageRequest:
|
||||
"""Represents a request for a page from the Reticulum network.
|
||||
@@ -22,6 +22,7 @@ class PageRequest:
|
||||
page_path: str
|
||||
field_data: dict | None = None
|
||||
|
||||
|
||||
class PageFetcher:
|
||||
"""Fetcher to download pages from the Reticulum network."""
|
||||
|
||||
@@ -43,7 +44,9 @@ class PageFetcher:
|
||||
Exception: If no path to destination or identity not found.
|
||||
|
||||
"""
|
||||
RNS.log(f"PageFetcher: starting fetch of {req.page_path} from {req.destination_hash}")
|
||||
RNS.log(
|
||||
f"PageFetcher: starting fetch of {req.page_path} from {req.destination_hash}"
|
||||
)
|
||||
dest_bytes = bytes.fromhex(req.destination_hash)
|
||||
if not RNS.Transport.has_path(dest_bytes):
|
||||
RNS.Transport.request_path(dest_bytes)
|
||||
@@ -79,9 +82,16 @@ class PageFetcher:
|
||||
ev.set()
|
||||
|
||||
link.set_link_established_callback(
|
||||
lambda link: link.request(req.page_path, req.field_data, response_callback=on_response, failed_callback=on_failed)
|
||||
lambda link: link.request(
|
||||
req.page_path,
|
||||
req.field_data,
|
||||
response_callback=on_response,
|
||||
failed_callback=on_failed,
|
||||
)
|
||||
)
|
||||
ev.wait(timeout=15)
|
||||
data_str = result["data"] or "No content received"
|
||||
RNS.log(f"PageFetcher: received data for {req.destination_hash}:{req.page_path}")
|
||||
RNS.log(
|
||||
f"PageFetcher: received data for {req.destination_hash}:{req.page_path}"
|
||||
)
|
||||
return data_str
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
Provides rendering capabilities for micron markup content,
|
||||
currently implemented as a placeholder.
|
||||
"""
|
||||
|
||||
import flet as ft
|
||||
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
Provides fallback rendering for plaintext content and source viewing.
|
||||
"""
|
||||
|
||||
import flet as ft
|
||||
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
Provides persistent storage for configuration, bookmarks, history,
|
||||
and other application data across different platforms.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import pathlib
|
||||
@@ -36,9 +37,11 @@ class StorageManager:
|
||||
pass
|
||||
|
||||
if os.name == "posix" and "ANDROID_ROOT" in os.environ:
|
||||
# Android - use app's private files directory
|
||||
storage_dir = pathlib.Path("/data/data/com.ren_browser/files")
|
||||
elif hasattr(os, "uname") and "iOS" in str(getattr(os, "uname", lambda: "")()).replace("iPhone", "iOS"):
|
||||
# Android - use user-accessible external storage
|
||||
storage_dir = pathlib.Path("/storage/emulated/0/Documents/ren_browser")
|
||||
elif hasattr(os, "uname") and "iOS" in str(
|
||||
getattr(os, "uname", lambda: "")()
|
||||
).replace("iPhone", "iOS"):
|
||||
# iOS - use app's documents directory
|
||||
storage_dir = pathlib.Path.home() / "Documents" / "ren_browser"
|
||||
else:
|
||||
@@ -46,7 +49,9 @@ class StorageManager:
|
||||
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"
|
||||
storage_dir = (
|
||||
pathlib.Path(os.environ["XDG_CONFIG_HOME"]) / "ren_browser"
|
||||
)
|
||||
else:
|
||||
storage_dir = pathlib.Path.home() / ".ren_browser"
|
||||
|
||||
@@ -58,6 +63,7 @@ class StorageManager:
|
||||
self._storage_dir.mkdir(parents=True, exist_ok=True)
|
||||
except (OSError, PermissionError):
|
||||
import tempfile
|
||||
|
||||
self._storage_dir = pathlib.Path(tempfile.gettempdir()) / "ren_browser"
|
||||
self._storage_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
@@ -65,17 +71,21 @@ class StorageManager:
|
||||
"""Get the path to the main configuration file."""
|
||||
return self._storage_dir / "config"
|
||||
|
||||
@staticmethod
|
||||
def get_reticulum_config_path() -> pathlib.Path:
|
||||
def get_reticulum_config_path(self) -> pathlib.Path:
|
||||
"""Get the path to the Reticulum configuration directory."""
|
||||
# Check for global override from app
|
||||
try:
|
||||
from ren_browser.app import RNS_CONFIG_DIR
|
||||
|
||||
if RNS_CONFIG_DIR:
|
||||
return pathlib.Path(RNS_CONFIG_DIR)
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
# On Android, use app storage directory instead of ~/.reticulum
|
||||
if os.name == "posix" and "ANDROID_ROOT" in os.environ:
|
||||
return self._storage_dir / "reticulum"
|
||||
|
||||
# Default to standard RNS config directory
|
||||
return pathlib.Path.home() / ".reticulum"
|
||||
|
||||
@@ -90,6 +100,7 @@ class StorageManager:
|
||||
|
||||
"""
|
||||
try:
|
||||
# Always save to client storage first (most reliable on mobile)
|
||||
if self.page and hasattr(self.page, "client_storage"):
|
||||
self.page.client_storage.set("ren_browser_config", config_content)
|
||||
|
||||
@@ -100,6 +111,7 @@ class StorageManager:
|
||||
|
||||
# Also save to local config path as backup
|
||||
config_path = self.get_config_path()
|
||||
config_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
config_path.write_text(config_content, encoding="utf-8")
|
||||
return True
|
||||
|
||||
@@ -111,7 +123,9 @@ class StorageManager:
|
||||
try:
|
||||
if self.page and hasattr(self.page, "client_storage"):
|
||||
self.page.client_storage.set("ren_browser_config", config_content)
|
||||
self.page.client_storage.set("ren_browser_config_error", f"File save failed: {error}")
|
||||
self.page.client_storage.set(
|
||||
"ren_browser_config_error", f"File save failed: {error}"
|
||||
)
|
||||
return True
|
||||
|
||||
try:
|
||||
@@ -122,6 +136,7 @@ class StorageManager:
|
||||
pass
|
||||
|
||||
import tempfile
|
||||
|
||||
temp_path = pathlib.Path(tempfile.gettempdir()) / "ren_browser_config.txt"
|
||||
temp_path.write_text(config_content, encoding="utf-8")
|
||||
return True
|
||||
@@ -136,6 +151,13 @@ class StorageManager:
|
||||
Configuration text, or empty string if not found
|
||||
|
||||
"""
|
||||
# On Android, prioritize client storage first as it's more reliable
|
||||
if os.name == "posix" and "ANDROID_ROOT" in os.environ:
|
||||
if self.page and hasattr(self.page, "client_storage"):
|
||||
stored_config = self.page.client_storage.get("ren_browser_config")
|
||||
if stored_config:
|
||||
return stored_config
|
||||
|
||||
try:
|
||||
reticulum_config_path = self.get_reticulum_config_path() / "config"
|
||||
if reticulum_config_path.exists():
|
||||
@@ -145,13 +167,18 @@ class StorageManager:
|
||||
if config_path.exists():
|
||||
return config_path.read_text(encoding="utf-8")
|
||||
|
||||
# Fallback to client storage for non-Android or if files don't exist
|
||||
if self.page and hasattr(self.page, "client_storage"):
|
||||
stored_config = self.page.client_storage.get("ren_browser_config")
|
||||
if stored_config:
|
||||
return stored_config
|
||||
|
||||
except (OSError, PermissionError, UnicodeDecodeError):
|
||||
pass
|
||||
# If file access fails, try client storage as fallback
|
||||
if self.page and hasattr(self.page, "client_storage"):
|
||||
stored_config = self.page.client_storage.get("ren_browser_config")
|
||||
if stored_config:
|
||||
return stored_config
|
||||
|
||||
return ""
|
||||
|
||||
@@ -163,7 +190,9 @@ class StorageManager:
|
||||
json.dump(bookmarks, f, indent=2)
|
||||
|
||||
if self.page and hasattr(self.page, "client_storage"):
|
||||
self.page.client_storage.set("ren_browser_bookmarks", json.dumps(bookmarks))
|
||||
self.page.client_storage.set(
|
||||
"ren_browser_bookmarks", json.dumps(bookmarks)
|
||||
)
|
||||
|
||||
return True
|
||||
except Exception:
|
||||
@@ -267,6 +296,7 @@ def get_rns_config_directory() -> str:
|
||||
"""Get the RNS config directory, checking for global override."""
|
||||
try:
|
||||
from ren_browser.app import RNS_CONFIG_DIR
|
||||
|
||||
if RNS_CONFIG_DIR:
|
||||
return RNS_CONFIG_DIR
|
||||
except ImportError:
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
Provides tab creation, switching, and content management functionality
|
||||
for the browser interface.
|
||||
"""
|
||||
|
||||
from types import SimpleNamespace
|
||||
|
||||
import flet as ft
|
||||
@@ -17,7 +18,7 @@ class TabsManager:
|
||||
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.
|
||||
|
||||
Args:
|
||||
@@ -25,27 +26,106 @@ class TabsManager:
|
||||
|
||||
"""
|
||||
import ren_browser.app as app_module
|
||||
|
||||
self.page = page
|
||||
self.page.on_resize = self._on_resize
|
||||
self.manager = SimpleNamespace(tabs=[], index=0)
|
||||
self.tab_bar = ft.Row(spacing=4)
|
||||
self.content_container = ft.Container(expand=True, bgcolor=ft.Colors.BLACK, padding=ft.padding.all(5))
|
||||
self.tab_bar = ft.Row(
|
||||
spacing=4,
|
||||
)
|
||||
self.overflow_menu = None
|
||||
self.content_container = ft.Container(
|
||||
expand=True, bgcolor=ft.Colors.BLACK, padding=ft.padding.all(5),
|
||||
)
|
||||
|
||||
default_content = render_micron("Welcome to Ren Browser") if app_module.RENDERER == "micron" else render_plaintext("Welcome to Ren Browser")
|
||||
default_content = (
|
||||
render_micron("Welcome to Ren Browser")
|
||||
if app_module.RENDERER == "micron"
|
||||
else render_plaintext("Welcome to Ren Browser")
|
||||
)
|
||||
self._add_tab_internal("Home", default_content)
|
||||
self.add_btn = ft.IconButton(ft.Icons.ADD, tooltip="New Tab", on_click=self._on_add_click)
|
||||
self.close_btn = ft.IconButton(ft.Icons.CLOSE, tooltip="Close Tab", on_click=self._on_close_click)
|
||||
self.tab_bar.controls.extend([self.add_btn, self.close_btn])
|
||||
self.add_btn = ft.IconButton(
|
||||
ft.Icons.ADD, tooltip="New Tab", on_click=self._on_add_click,
|
||||
)
|
||||
self.close_btn = ft.IconButton(
|
||||
ft.Icons.CLOSE, tooltip="Close Tab", on_click=self._on_close_click,
|
||||
)
|
||||
self.tab_bar.controls.append(self.add_btn)
|
||||
self.tab_bar.controls.append(self.close_btn)
|
||||
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 _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.controls:
|
||||
self.tab_bar.controls.remove(self.overflow_menu)
|
||||
self.overflow_menu = None
|
||||
|
||||
"""Estimate available width for tabs (Page width - buttons - padding)."""
|
||||
available_width = self.page.width - 100
|
||||
|
||||
cumulative_width = 0
|
||||
visible_tabs_count = 0
|
||||
|
||||
tab_containers = [c for c in self.tab_bar.controls if isinstance(c, ft.Container)]
|
||||
|
||||
for i, tab in enumerate(self.manager.tabs):
|
||||
"""Estimate tab width: (char count * avg char width) + padding + spacing."""
|
||||
estimated_width = len(tab["title"]) * 10 + 32 + self.tab_bar.spacing
|
||||
|
||||
"""Always show at least one tab."""
|
||||
if cumulative_width + estimated_width <= available_width or i == 0:
|
||||
cumulative_width += estimated_width
|
||||
if i < len(tab_containers):
|
||||
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:
|
||||
"""Move extra tabs to overflow menu."""
|
||||
overflow_items = []
|
||||
for i in range(visible_tabs_count, len(self.manager.tabs)):
|
||||
tab_data = self.manager.tabs[i]
|
||||
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.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)
|
||||
url_field = ft.TextField(
|
||||
value=title,
|
||||
expand=True,
|
||||
text_style=ft.TextStyle(size=12),
|
||||
content_padding=ft.padding.only(top=8, bottom=8, left=8, right=8)
|
||||
content_padding=ft.padding.only(top=8, bottom=8, left=8, right=8),
|
||||
)
|
||||
go_btn = ft.IconButton(
|
||||
ft.Icons.OPEN_IN_BROWSER,
|
||||
tooltip="Load URL",
|
||||
on_click=lambda e, i=idx: self._on_tab_go(e, i),
|
||||
)
|
||||
go_btn = ft.IconButton(ft.Icons.OPEN_IN_BROWSER, tooltip="Load URL", on_click=lambda e, i=idx: self._on_tab_go(e, i))
|
||||
content_control = content
|
||||
tab_content = ft.Column(
|
||||
expand=True,
|
||||
@@ -53,45 +133,64 @@ class TabsManager:
|
||||
content_control,
|
||||
],
|
||||
)
|
||||
self.manager.tabs.append({
|
||||
"title": title,
|
||||
"url_field": url_field,
|
||||
"go_btn": go_btn,
|
||||
"content_control": content_control,
|
||||
"content": tab_content,
|
||||
})
|
||||
btn = ft.Container(
|
||||
self.manager.tabs.append(
|
||||
{
|
||||
"title": title,
|
||||
"url_field": url_field,
|
||||
"go_btn": go_btn,
|
||||
"content_control": content_control,
|
||||
"content": tab_content,
|
||||
},
|
||||
)
|
||||
tab_container = ft.Container(
|
||||
content=ft.Text(title),
|
||||
on_click=lambda e, i=idx: self.select_tab(i),
|
||||
on_click=lambda e, i=idx: self.select_tab(i), # type: ignore
|
||||
padding=ft.padding.symmetric(horizontal=12, vertical=6),
|
||||
border_radius=5,
|
||||
bgcolor=ft.Colors.SURFACE_CONTAINER_HIGHEST,
|
||||
)
|
||||
"""Insert the new tab before the add/close buttons."""
|
||||
insert_pos = max(0, len(self.tab_bar.controls) - 2)
|
||||
self.tab_bar.controls.insert(insert_pos, btn)
|
||||
self.tab_bar.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}"
|
||||
content_text = f"Content for {title}"
|
||||
import ren_browser.app as app_module
|
||||
content = render_micron(content_text) if app_module.RENDERER == "micron" else render_plaintext(content_text)
|
||||
|
||||
content = (
|
||||
render_micron(content_text)
|
||||
if app_module.RENDERER == "micron"
|
||||
else render_plaintext(content_text)
|
||||
)
|
||||
self._add_tab_internal(title, content)
|
||||
self.select_tab(len(self.manager.tabs) - 1)
|
||||
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:
|
||||
return
|
||||
idx = self.manager.index
|
||||
|
||||
tab_containers = [c for c in self.tab_bar.controls if isinstance(c, ft.Container)]
|
||||
control_to_remove = tab_containers[idx]
|
||||
|
||||
self.manager.tabs.pop(idx)
|
||||
self.tab_bar.controls.pop(idx)
|
||||
for i, control in enumerate(self.tab_bar.controls[:-2]):
|
||||
control.on_click = lambda e, i=i: self.select_tab(i)
|
||||
self.tab_bar.controls.remove(control_to_remove)
|
||||
|
||||
updated_tab_containers = [c for c in self.tab_bar.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)
|
||||
self.select_tab(new_idx)
|
||||
self._update_tab_visibility()
|
||||
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.
|
||||
|
||||
Args:
|
||||
@@ -99,22 +198,31 @@ class TabsManager:
|
||||
|
||||
"""
|
||||
self.manager.index = idx
|
||||
for i, control in enumerate(self.tab_bar.controls[:-2]):
|
||||
|
||||
tab_containers = [c for c in self.tab_bar.controls if isinstance(c, ft.Container)]
|
||||
for i, control in enumerate(tab_containers):
|
||||
if i == idx:
|
||||
control.bgcolor = ft.Colors.PRIMARY_CONTAINER
|
||||
else:
|
||||
control.bgcolor = ft.Colors.SURFACE_CONTAINER_HIGHEST
|
||||
|
||||
self.content_container.content = self.manager.tabs[idx]["content"]
|
||||
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]
|
||||
url = tab["url_field"].value.strip()
|
||||
if not url:
|
||||
return
|
||||
placeholder_text = f"Loading content for {url}"
|
||||
import ren_browser.app as app_module
|
||||
new_control = render_micron(placeholder_text) if app_module.RENDERER == "micron" else render_plaintext(placeholder_text)
|
||||
|
||||
new_control = (
|
||||
render_micron(placeholder_text)
|
||||
if app_module.RENDERER == "micron"
|
||||
else render_plaintext(placeholder_text)
|
||||
)
|
||||
tab["content_control"] = new_control
|
||||
tab["content"].controls[0] = new_control
|
||||
if self.manager.index == idx:
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
Provides configuration management, log viewing, and storage
|
||||
information display.
|
||||
"""
|
||||
|
||||
import flet as ft
|
||||
|
||||
from ren_browser.logs import ERROR_LOGS, RET_LOGS
|
||||
@@ -35,13 +36,23 @@ def open_settings_tab(page: ft.Page, tab_manager):
|
||||
try:
|
||||
success = storage.save_config(config_field.value)
|
||||
if success:
|
||||
page.snack_bar = ft.SnackBar(ft.Text("Config saved successfully. Please restart the app."), open=True)
|
||||
print("Config saved successfully. Please restart the app.")
|
||||
page.snack_bar = ft.SnackBar(
|
||||
ft.Text("Config saved successfully. Please restart the app."),
|
||||
open=True,
|
||||
)
|
||||
else:
|
||||
page.snack_bar = ft.SnackBar(ft.Text("Error saving config: Storage operation failed"), open=True)
|
||||
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:
|
||||
page.snack_bar = ft.SnackBar(ft.Text(f"Error saving config: {ex}"), open=True)
|
||||
page.update()
|
||||
save_btn = ft.ElevatedButton("Save and Restart", on_click=on_save_config)
|
||||
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)
|
||||
error_field = ft.TextField(
|
||||
label="Error Logs",
|
||||
value="",
|
||||
@@ -69,22 +80,29 @@ def open_settings_tab(page: ft.Page, tab_manager):
|
||||
)
|
||||
|
||||
content_placeholder = ft.Container(expand=True)
|
||||
|
||||
def show_config(ev):
|
||||
content_placeholder.content = config_field
|
||||
page.update()
|
||||
|
||||
def show_errors(ev):
|
||||
error_field.value = "\n".join(ERROR_LOGS) or "No errors logged."
|
||||
content_placeholder.content = error_field
|
||||
page.update()
|
||||
|
||||
def show_ret_logs(ev):
|
||||
ret_field.value = "\n".join(RET_LOGS) or "No Reticulum logs."
|
||||
content_placeholder.content = ret_field
|
||||
page.update()
|
||||
|
||||
def show_storage_info(ev):
|
||||
storage_info = storage.get_storage_info()
|
||||
storage_field.value = "\n".join([f"{key}: {value}" for key, value in storage_info.items()])
|
||||
storage_field.value = "\n".join(
|
||||
[f"{key}: {value}" for key, value in storage_info.items()]
|
||||
)
|
||||
content_placeholder.content = storage_field
|
||||
page.update()
|
||||
|
||||
def refresh_current_view(ev):
|
||||
# Refresh the currently displayed content
|
||||
if content_placeholder.content == error_field:
|
||||
@@ -95,12 +113,15 @@ def open_settings_tab(page: ft.Page, tab_manager):
|
||||
show_storage_info(ev)
|
||||
elif content_placeholder.content == config_field:
|
||||
show_config(ev)
|
||||
|
||||
btn_config = ft.ElevatedButton("Config", on_click=show_config)
|
||||
btn_errors = ft.ElevatedButton("Errors", on_click=show_errors)
|
||||
btn_ret = ft.ElevatedButton("Ret Logs", on_click=show_ret_logs)
|
||||
btn_storage = ft.ElevatedButton("Storage", on_click=show_storage_info)
|
||||
btn_refresh = ft.ElevatedButton("Refresh", on_click=refresh_current_view)
|
||||
button_row = ft.Row(controls=[btn_config, btn_errors, btn_ret, btn_storage, btn_refresh])
|
||||
button_row = ft.Row(
|
||||
controls=[btn_config, btn_errors, btn_ret, btn_storage, btn_refresh]
|
||||
)
|
||||
content_placeholder.content = config_field
|
||||
settings_content = ft.Column(
|
||||
expand=True,
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
Builds the complete browser interface including tabs, navigation,
|
||||
announce handling, and content rendering.
|
||||
"""
|
||||
|
||||
import flet as ft
|
||||
from flet import Page
|
||||
|
||||
@@ -27,10 +28,12 @@ def build_ui(page: Page):
|
||||
|
||||
page_fetcher = PageFetcher()
|
||||
announce_list = ft.ListView(expand=True, spacing=1)
|
||||
|
||||
def update_announces(ann_list):
|
||||
announce_list.controls.clear()
|
||||
for ann in ann_list:
|
||||
label = ann.display_name or ann.destination_hash
|
||||
|
||||
def on_click_ann(e, dest=ann.destination_hash, disp=ann.display_name):
|
||||
title = disp or "Anonymous"
|
||||
full_url = f"{dest}:/page/index.mu"
|
||||
@@ -41,12 +44,14 @@ def build_ui(page: Page):
|
||||
tab["url_field"].value = full_url
|
||||
tab_manager.select_tab(idx)
|
||||
page.update()
|
||||
|
||||
def fetch_and_update():
|
||||
req = PageRequest(destination_hash=dest, page_path="/page/index.mu")
|
||||
try:
|
||||
result = page_fetcher.fetch_page(req)
|
||||
except Exception as ex:
|
||||
import ren_browser.app as app_module
|
||||
|
||||
app_module.log_error(str(ex))
|
||||
result = f"Error: {ex}"
|
||||
try:
|
||||
@@ -62,13 +67,21 @@ def build_ui(page: Page):
|
||||
if tab_manager.manager.index == idx:
|
||||
tab_manager.content_container.content = tab["content"]
|
||||
page.update()
|
||||
|
||||
page.run_thread(fetch_and_update)
|
||||
|
||||
announce_list.controls.append(ft.TextButton(label, on_click=on_click_ann))
|
||||
page.update()
|
||||
|
||||
AnnounceService(update_callback=update_announces)
|
||||
page.drawer = ft.NavigationDrawer(
|
||||
controls=[
|
||||
ft.Text("Announcements", weight=ft.FontWeight.BOLD, text_align=ft.TextAlign.CENTER, expand=True),
|
||||
ft.Text(
|
||||
"Announcements",
|
||||
weight=ft.FontWeight.BOLD,
|
||||
text_align=ft.TextAlign.CENTER,
|
||||
expand=True,
|
||||
),
|
||||
ft.Divider(),
|
||||
announce_list,
|
||||
],
|
||||
@@ -76,12 +89,22 @@ def build_ui(page: Page):
|
||||
page.appbar.leading = ft.IconButton(
|
||||
ft.Icons.MENU,
|
||||
tooltip="Toggle sidebar",
|
||||
on_click=lambda e: (setattr(page.drawer, "open", not page.drawer.open), page.update()),
|
||||
on_click=lambda e: (
|
||||
setattr(page.drawer, "open", not page.drawer.open),
|
||||
page.update(),
|
||||
),
|
||||
)
|
||||
|
||||
tab_manager = TabsManager(page)
|
||||
from ren_browser.ui.settings import open_settings_tab
|
||||
page.appbar.actions = [ft.IconButton(ft.Icons.SETTINGS, tooltip="Settings", on_click=lambda e: open_settings_tab(page, tab_manager))]
|
||||
|
||||
page.appbar.actions = [
|
||||
ft.IconButton(
|
||||
ft.Icons.SETTINGS,
|
||||
tooltip="Settings",
|
||||
on_click=lambda e: open_settings_tab(page, tab_manager),
|
||||
)
|
||||
]
|
||||
Shortcuts(page, tab_manager)
|
||||
url_bar = ft.Row(
|
||||
controls=[
|
||||
@@ -91,15 +114,19 @@ def build_ui(page: Page):
|
||||
)
|
||||
page.appbar.title = url_bar
|
||||
orig_select_tab = tab_manager.select_tab
|
||||
|
||||
def _select_tab_and_update_url(i):
|
||||
orig_select_tab(i)
|
||||
tab = tab_manager.manager.tabs[i]
|
||||
url_bar.controls.clear()
|
||||
url_bar.controls.extend([tab["url_field"], tab["go_btn"]])
|
||||
page.update()
|
||||
|
||||
tab_manager.select_tab = _select_tab_and_update_url
|
||||
|
||||
def _update_content_width(e=None):
|
||||
tab_manager.content_container.width = page.width
|
||||
|
||||
_update_content_width()
|
||||
page.on_resized = lambda e: (_update_content_width(), page.update())
|
||||
main_area = ft.Column(
|
||||
|
||||
@@ -36,6 +36,7 @@ def mock_rns():
|
||||
|
||||
# Mock at the module level for all imports
|
||||
import sys
|
||||
|
||||
sys.modules["RNS"] = mock_rns
|
||||
|
||||
yield mock_rns
|
||||
@@ -51,7 +52,7 @@ def sample_announce_data():
|
||||
return {
|
||||
"destination_hash": "1234567890abcdef",
|
||||
"display_name": "Test Node",
|
||||
"timestamp": 1234567890
|
||||
"timestamp": 1234567890,
|
||||
}
|
||||
|
||||
|
||||
@@ -59,10 +60,9 @@ def sample_announce_data():
|
||||
def sample_page_request():
|
||||
"""Sample page request for testing."""
|
||||
from ren_browser.pages.page_request import PageRequest
|
||||
|
||||
return PageRequest(
|
||||
destination_hash="1234567890abcdef",
|
||||
page_path="/page/index.mu",
|
||||
field_data=None
|
||||
destination_hash="1234567890abcdef", page_path="/page/index.mu", field_data=None
|
||||
)
|
||||
|
||||
|
||||
@@ -75,11 +75,11 @@ def mock_storage_manager():
|
||||
mock_storage.get_config_path.return_value = Mock()
|
||||
mock_storage.get_reticulum_config_path.return_value = Mock()
|
||||
mock_storage.get_storage_info.return_value = {
|
||||
'storage_dir': '/mock/storage',
|
||||
'config_path': '/mock/storage/config.txt',
|
||||
'reticulum_config_path': '/mock/storage/reticulum',
|
||||
'storage_dir_exists': True,
|
||||
'storage_dir_writable': True,
|
||||
'has_client_storage': True,
|
||||
"storage_dir": "/mock/storage",
|
||||
"config_path": "/mock/storage/config.txt",
|
||||
"reticulum_config_path": "/mock/storage/reticulum",
|
||||
"storage_dir_exists": True,
|
||||
"storage_dir_writable": True,
|
||||
"has_client_storage": True,
|
||||
}
|
||||
return mock_storage
|
||||
|
||||
@@ -28,8 +28,14 @@ class TestAppIntegration:
|
||||
def test_entry_points_exist(self):
|
||||
"""Test that all expected entry points exist and are callable."""
|
||||
entry_points = [
|
||||
"run", "web", "android", "ios",
|
||||
"run_dev", "web_dev", "android_dev", "ios_dev"
|
||||
"run",
|
||||
"web",
|
||||
"android",
|
||||
"ios",
|
||||
"run_dev",
|
||||
"web_dev",
|
||||
"android_dev",
|
||||
"ios_dev",
|
||||
]
|
||||
|
||||
for entry_point in entry_points:
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
from ren_browser.announces.announces import Announce
|
||||
|
||||
|
||||
@@ -10,7 +9,7 @@ class TestAnnounce:
|
||||
announce = Announce(
|
||||
destination_hash="1234567890abcdef",
|
||||
display_name="Test Node",
|
||||
timestamp=1234567890
|
||||
timestamp=1234567890,
|
||||
)
|
||||
|
||||
assert announce.destination_hash == "1234567890abcdef"
|
||||
@@ -20,18 +19,17 @@ class TestAnnounce:
|
||||
def test_announce_with_none_display_name(self):
|
||||
"""Test Announce creation with None display name."""
|
||||
announce = Announce(
|
||||
destination_hash="1234567890abcdef",
|
||||
display_name=None,
|
||||
timestamp=1234567890
|
||||
destination_hash="1234567890abcdef", display_name=None, timestamp=1234567890
|
||||
)
|
||||
|
||||
assert announce.destination_hash == "1234567890abcdef"
|
||||
assert announce.display_name is None
|
||||
assert announce.timestamp == 1234567890
|
||||
|
||||
|
||||
class TestAnnounceService:
|
||||
"""Test cases for the AnnounceService class.
|
||||
|
||||
|
||||
Note: These tests are simplified due to complex RNS integration.
|
||||
Full integration tests will be added in the future.
|
||||
"""
|
||||
|
||||
@@ -35,9 +35,7 @@ class TestApp:
|
||||
|
||||
def test_run_with_default_args(self, mock_rns):
|
||||
"""Test run function with default arguments."""
|
||||
with patch("sys.argv", ["ren-browser"]), \
|
||||
patch("flet.app") as mock_ft_app:
|
||||
|
||||
with patch("sys.argv", ["ren-browser"]), patch("flet.app") as mock_ft_app:
|
||||
app.run()
|
||||
|
||||
mock_ft_app.assert_called_once()
|
||||
@@ -46,9 +44,10 @@ class TestApp:
|
||||
|
||||
def test_run_with_web_flag(self, mock_rns):
|
||||
"""Test run function with web flag."""
|
||||
with patch("sys.argv", ["ren-browser", "--web"]), \
|
||||
patch("flet.app") as mock_ft_app:
|
||||
|
||||
with (
|
||||
patch("sys.argv", ["ren-browser", "--web"]),
|
||||
patch("flet.app") as mock_ft_app,
|
||||
):
|
||||
app.run()
|
||||
|
||||
mock_ft_app.assert_called_once()
|
||||
@@ -58,9 +57,10 @@ class TestApp:
|
||||
|
||||
def test_run_with_web_and_port(self, mock_rns):
|
||||
"""Test run function with web flag and custom port."""
|
||||
with patch("sys.argv", ["ren-browser", "--web", "--port", "8080"]), \
|
||||
patch("flet.app") as mock_ft_app:
|
||||
|
||||
with (
|
||||
patch("sys.argv", ["ren-browser", "--web", "--port", "8080"]),
|
||||
patch("flet.app") as mock_ft_app,
|
||||
):
|
||||
app.run()
|
||||
|
||||
mock_ft_app.assert_called_once()
|
||||
@@ -71,9 +71,10 @@ class TestApp:
|
||||
|
||||
def test_run_with_renderer_flag(self, mock_rns):
|
||||
"""Test run function with renderer selection."""
|
||||
with patch("sys.argv", ["ren-browser", "--renderer", "micron"]), \
|
||||
patch("flet.app"):
|
||||
|
||||
with (
|
||||
patch("sys.argv", ["ren-browser", "--renderer", "micron"]),
|
||||
patch("flet.app"),
|
||||
):
|
||||
app.run()
|
||||
|
||||
assert app.RENDERER == "micron"
|
||||
@@ -131,8 +132,10 @@ class TestApp:
|
||||
"""Test that RENDERER global is properly updated."""
|
||||
original_renderer = app.RENDERER
|
||||
|
||||
with patch("sys.argv", ["ren-browser", "--renderer", "micron"]), \
|
||||
patch("flet.app"):
|
||||
with (
|
||||
patch("sys.argv", ["ren-browser", "--renderer", "micron"]),
|
||||
patch("flet.app"),
|
||||
):
|
||||
app.run()
|
||||
assert app.RENDERER == "micron"
|
||||
|
||||
|
||||
@@ -58,7 +58,9 @@ class TestLogsModule:
|
||||
|
||||
assert len(logs.RET_LOGS) == 1
|
||||
assert logs.RET_LOGS[0] == "[2023-01-01T12:00:00] Test RNS message"
|
||||
logs._original_rns_log.assert_called_once_with("Test RNS message", "arg1", kwarg1="value1")
|
||||
logs._original_rns_log.assert_called_once_with(
|
||||
"Test RNS message", "arg1", kwarg1="value1"
|
||||
)
|
||||
assert result == "original_result"
|
||||
|
||||
def test_multiple_log_calls(self):
|
||||
|
||||
@@ -7,8 +7,7 @@ class TestPageRequest:
|
||||
def test_page_request_creation(self):
|
||||
"""Test basic PageRequest creation."""
|
||||
request = PageRequest(
|
||||
destination_hash="1234567890abcdef",
|
||||
page_path="/page/index.mu"
|
||||
destination_hash="1234567890abcdef", page_path="/page/index.mu"
|
||||
)
|
||||
|
||||
assert request.destination_hash == "1234567890abcdef"
|
||||
@@ -21,7 +20,7 @@ class TestPageRequest:
|
||||
request = PageRequest(
|
||||
destination_hash="1234567890abcdef",
|
||||
page_path="/page/form.mu",
|
||||
field_data=field_data
|
||||
field_data=field_data,
|
||||
)
|
||||
|
||||
assert request.destination_hash == "1234567890abcdef"
|
||||
@@ -48,7 +47,7 @@ class TestPageRequest:
|
||||
# These will be implemented when the networking layer is more stable.
|
||||
class TestPageFetcher:
|
||||
"""Test cases for the PageFetcher class.
|
||||
|
||||
|
||||
Note: These tests are simplified due to complex RNS networking integration.
|
||||
Full integration tests will be added when the networking layer is stable.
|
||||
"""
|
||||
@@ -59,7 +58,7 @@ class TestPageFetcher:
|
||||
requests = [
|
||||
PageRequest("hash1", "/index.mu"),
|
||||
PageRequest("hash2", "/about.mu", {"form": "data"}),
|
||||
PageRequest("hash3", "/contact.mu")
|
||||
PageRequest("hash3", "/contact.mu"),
|
||||
]
|
||||
|
||||
# Test that requests have the expected structure
|
||||
|
||||
@@ -57,7 +57,7 @@ class TestPlaintextRenderer:
|
||||
|
||||
class TestMicronRenderer:
|
||||
"""Test cases for the micron renderer.
|
||||
|
||||
|
||||
Note: The micron renderer is currently a placeholder implementation
|
||||
that displays raw content without markup processing.
|
||||
"""
|
||||
|
||||
@@ -215,7 +215,7 @@ class TestShortcuts:
|
||||
url_field2 = Mock()
|
||||
mock_tab_manager.manager.tabs = [
|
||||
{"url_field": url_field1},
|
||||
{"url_field": url_field2}
|
||||
{"url_field": url_field2},
|
||||
]
|
||||
mock_tab_manager.manager.index = 1 # Second tab
|
||||
|
||||
|
||||
@@ -5,7 +5,11 @@ from unittest.mock import Mock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from ren_browser.storage.storage import StorageManager, get_storage_manager, initialize_storage
|
||||
from ren_browser.storage.storage import (
|
||||
StorageManager,
|
||||
get_storage_manager,
|
||||
initialize_storage,
|
||||
)
|
||||
|
||||
|
||||
class TestStorageManager:
|
||||
@@ -13,13 +17,15 @@ class TestStorageManager:
|
||||
|
||||
def test_storage_manager_init_without_page(self):
|
||||
"""Test StorageManager initialization without a page."""
|
||||
with patch('ren_browser.storage.storage.StorageManager._get_storage_directory') as mock_get_dir:
|
||||
mock_dir = Path('/mock/storage')
|
||||
with patch(
|
||||
"ren_browser.storage.storage.StorageManager._get_storage_directory"
|
||||
) as mock_get_dir:
|
||||
mock_dir = Path("/mock/storage")
|
||||
mock_get_dir.return_value = mock_dir
|
||||
|
||||
with patch('pathlib.Path.mkdir') as mock_mkdir:
|
||||
|
||||
with patch("pathlib.Path.mkdir") as mock_mkdir:
|
||||
storage = StorageManager()
|
||||
|
||||
|
||||
assert storage.page is None
|
||||
assert storage._storage_dir == mock_dir
|
||||
mock_mkdir.assert_called_once_with(parents=True, exist_ok=True)
|
||||
@@ -27,27 +33,34 @@ class TestStorageManager:
|
||||
def test_storage_manager_init_with_page(self):
|
||||
"""Test StorageManager initialization with a page."""
|
||||
mock_page = Mock()
|
||||
|
||||
with patch('ren_browser.storage.storage.StorageManager._get_storage_directory') as mock_get_dir:
|
||||
mock_dir = Path('/mock/storage')
|
||||
|
||||
with patch(
|
||||
"ren_browser.storage.storage.StorageManager._get_storage_directory"
|
||||
) as mock_get_dir:
|
||||
mock_dir = Path("/mock/storage")
|
||||
mock_get_dir.return_value = mock_dir
|
||||
|
||||
with patch('pathlib.Path.mkdir'):
|
||||
|
||||
with patch("pathlib.Path.mkdir"):
|
||||
storage = StorageManager(mock_page)
|
||||
|
||||
|
||||
assert storage.page == mock_page
|
||||
assert storage._storage_dir == mock_dir
|
||||
|
||||
def test_get_storage_directory_desktop(self):
|
||||
"""Test storage directory detection for desktop platforms."""
|
||||
with patch('os.name', 'posix'), \
|
||||
patch.dict('os.environ', {'XDG_CONFIG_HOME': '/home/user/.config'}, clear=True), \
|
||||
patch('pathlib.Path.mkdir'):
|
||||
|
||||
with patch('ren_browser.storage.storage.StorageManager._ensure_storage_directory'):
|
||||
with (
|
||||
patch("os.name", "posix"),
|
||||
patch.dict(
|
||||
"os.environ", {"XDG_CONFIG_HOME": "/home/user/.config"}, clear=True
|
||||
),
|
||||
patch("pathlib.Path.mkdir"),
|
||||
):
|
||||
with patch(
|
||||
"ren_browser.storage.storage.StorageManager._ensure_storage_directory"
|
||||
):
|
||||
storage = StorageManager()
|
||||
storage._storage_dir = storage._get_storage_directory()
|
||||
expected_dir = Path('/home/user/.config') / 'ren_browser'
|
||||
expected_dir = Path("/home/user/.config") / "ren_browser"
|
||||
assert storage._storage_dir == expected_dir
|
||||
|
||||
def test_get_storage_directory_windows(self):
|
||||
@@ -57,14 +70,17 @@ class TestStorageManager:
|
||||
|
||||
def test_get_storage_directory_android(self):
|
||||
"""Test storage directory detection for Android."""
|
||||
with patch('os.name', 'posix'), \
|
||||
patch.dict('os.environ', {'ANDROID_ROOT': '/system'}, clear=True), \
|
||||
patch('pathlib.Path.mkdir'):
|
||||
|
||||
with patch('ren_browser.storage.storage.StorageManager._ensure_storage_directory'):
|
||||
with (
|
||||
patch("os.name", "posix"),
|
||||
patch.dict("os.environ", {"ANDROID_ROOT": "/system"}, clear=True),
|
||||
patch("pathlib.Path.mkdir"),
|
||||
):
|
||||
with patch(
|
||||
"ren_browser.storage.storage.StorageManager._ensure_storage_directory"
|
||||
):
|
||||
storage = StorageManager()
|
||||
storage._storage_dir = storage._get_storage_directory()
|
||||
expected_dir = Path('/data/data/com.ren_browser/files')
|
||||
expected_dir = Path("/storage/emulated/0/Documents/ren_browser")
|
||||
assert storage._storage_dir == expected_dir
|
||||
|
||||
def test_get_config_path(self):
|
||||
@@ -72,17 +88,17 @@ class TestStorageManager:
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
storage = StorageManager()
|
||||
storage._storage_dir = Path(temp_dir)
|
||||
|
||||
|
||||
config_path = storage.get_config_path()
|
||||
expected_path = Path(temp_dir) / 'config'
|
||||
expected_path = Path(temp_dir) / "config"
|
||||
assert config_path == expected_path
|
||||
|
||||
def test_get_reticulum_config_path(self):
|
||||
"""Test getting Reticulum config directory path."""
|
||||
storage = StorageManager()
|
||||
|
||||
|
||||
config_path = storage.get_reticulum_config_path()
|
||||
expected_path = Path.home() / '.reticulum'
|
||||
expected_path = Path.home() / ".reticulum"
|
||||
assert config_path == expected_path
|
||||
|
||||
def test_save_config_success(self):
|
||||
@@ -90,52 +106,71 @@ class TestStorageManager:
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
storage = StorageManager()
|
||||
storage._storage_dir = Path(temp_dir)
|
||||
|
||||
|
||||
# Mock the reticulum config path to use temp dir
|
||||
with patch.object(storage, 'get_reticulum_config_path', return_value=Path(temp_dir) / "reticulum"):
|
||||
with patch.object(
|
||||
storage,
|
||||
"get_reticulum_config_path",
|
||||
return_value=Path(temp_dir) / "reticulum",
|
||||
):
|
||||
config_content = "test config content"
|
||||
result = storage.save_config(config_content)
|
||||
|
||||
|
||||
assert result is True
|
||||
config_path = storage.get_config_path()
|
||||
assert config_path.exists()
|
||||
assert config_path.read_text(encoding='utf-8') == config_content
|
||||
assert config_path.read_text(encoding="utf-8") == config_content
|
||||
|
||||
def test_save_config_with_client_storage(self):
|
||||
"""Test config saving with client storage."""
|
||||
mock_page = Mock()
|
||||
mock_page.client_storage.set = Mock()
|
||||
|
||||
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
storage = StorageManager(mock_page)
|
||||
storage._storage_dir = Path(temp_dir)
|
||||
|
||||
|
||||
# Mock the reticulum config path to use temp dir
|
||||
with patch.object(storage, 'get_reticulum_config_path', return_value=Path(temp_dir) / "reticulum"):
|
||||
with patch.object(
|
||||
storage,
|
||||
"get_reticulum_config_path",
|
||||
return_value=Path(temp_dir) / "reticulum",
|
||||
):
|
||||
config_content = "test config content"
|
||||
result = storage.save_config(config_content)
|
||||
|
||||
|
||||
assert result is True
|
||||
mock_page.client_storage.set.assert_called_with('ren_browser_config', config_content)
|
||||
mock_page.client_storage.set.assert_called_with(
|
||||
"ren_browser_config", config_content
|
||||
)
|
||||
|
||||
def test_save_config_fallback(self):
|
||||
"""Test config saving fallback when file system fails."""
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
mock_page = Mock()
|
||||
mock_page.client_storage.set = Mock()
|
||||
|
||||
|
||||
storage = StorageManager(mock_page)
|
||||
storage._storage_dir = Path(temp_dir)
|
||||
|
||||
|
||||
# Mock the reticulum config path to use temp dir and cause failure
|
||||
with patch.object(storage, 'get_reticulum_config_path', return_value=Path(temp_dir) / "reticulum"):
|
||||
with patch('pathlib.Path.write_text', side_effect=PermissionError("Access denied")):
|
||||
with patch.object(
|
||||
storage,
|
||||
"get_reticulum_config_path",
|
||||
return_value=Path(temp_dir) / "reticulum",
|
||||
):
|
||||
with patch(
|
||||
"pathlib.Path.write_text",
|
||||
side_effect=PermissionError("Access denied"),
|
||||
):
|
||||
config_content = "test config content"
|
||||
result = storage.save_config(config_content)
|
||||
|
||||
|
||||
assert result is True
|
||||
# Check that the config was set to client storage
|
||||
mock_page.client_storage.set.assert_any_call('ren_browser_config', config_content)
|
||||
mock_page.client_storage.set.assert_any_call(
|
||||
"ren_browser_config", config_content
|
||||
)
|
||||
# Verify that client storage was called at least once
|
||||
assert mock_page.client_storage.set.call_count >= 1
|
||||
|
||||
@@ -144,13 +179,17 @@ class TestStorageManager:
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
storage = StorageManager()
|
||||
storage._storage_dir = Path(temp_dir)
|
||||
|
||||
|
||||
# Mock the reticulum config path to use temp dir
|
||||
with patch.object(storage, 'get_reticulum_config_path', return_value=Path(temp_dir) / "reticulum"):
|
||||
with patch.object(
|
||||
storage,
|
||||
"get_reticulum_config_path",
|
||||
return_value=Path(temp_dir) / "reticulum",
|
||||
):
|
||||
config_content = "test config content"
|
||||
config_path = storage.get_config_path()
|
||||
config_path.write_text(config_content, encoding='utf-8')
|
||||
|
||||
config_path.write_text(config_content, encoding="utf-8")
|
||||
|
||||
loaded_config = storage.load_config()
|
||||
assert loaded_config == config_content
|
||||
|
||||
@@ -158,25 +197,33 @@ class TestStorageManager:
|
||||
"""Test loading config from client storage when file doesn't exist."""
|
||||
mock_page = Mock()
|
||||
mock_page.client_storage.get = Mock(return_value="client storage config")
|
||||
|
||||
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
storage = StorageManager(mock_page)
|
||||
storage._storage_dir = Path(temp_dir)
|
||||
|
||||
|
||||
# Mock the reticulum config path to also be in temp dir
|
||||
with patch.object(storage, 'get_reticulum_config_path', return_value=Path(temp_dir) / "reticulum"):
|
||||
with patch.object(
|
||||
storage,
|
||||
"get_reticulum_config_path",
|
||||
return_value=Path(temp_dir) / "reticulum",
|
||||
):
|
||||
loaded_config = storage.load_config()
|
||||
assert loaded_config == "client storage config"
|
||||
mock_page.client_storage.get.assert_called_with('ren_browser_config')
|
||||
mock_page.client_storage.get.assert_called_with("ren_browser_config")
|
||||
|
||||
def test_load_config_default(self):
|
||||
"""Test loading default config when no config exists."""
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
storage = StorageManager()
|
||||
storage._storage_dir = Path(temp_dir)
|
||||
|
||||
|
||||
# Mock the reticulum config path to also be in temp dir
|
||||
with patch.object(storage, 'get_reticulum_config_path', return_value=Path(temp_dir) / "reticulum"):
|
||||
with patch.object(
|
||||
storage,
|
||||
"get_reticulum_config_path",
|
||||
return_value=Path(temp_dir) / "reticulum",
|
||||
):
|
||||
loaded_config = storage.load_config()
|
||||
assert loaded_config == ""
|
||||
|
||||
@@ -185,15 +232,15 @@ class TestStorageManager:
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
storage = StorageManager()
|
||||
storage._storage_dir = Path(temp_dir)
|
||||
|
||||
|
||||
bookmarks = [{"name": "Test", "url": "test://example"}]
|
||||
result = storage.save_bookmarks(bookmarks)
|
||||
|
||||
|
||||
assert result is True
|
||||
bookmarks_path = storage._storage_dir / 'bookmarks.json'
|
||||
bookmarks_path = storage._storage_dir / "bookmarks.json"
|
||||
assert bookmarks_path.exists()
|
||||
|
||||
with open(bookmarks_path, 'r', encoding='utf-8') as f:
|
||||
|
||||
with open(bookmarks_path, "r", encoding="utf-8") as f:
|
||||
loaded_bookmarks = json.load(f)
|
||||
assert loaded_bookmarks == bookmarks
|
||||
|
||||
@@ -202,13 +249,13 @@ class TestStorageManager:
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
storage = StorageManager()
|
||||
storage._storage_dir = Path(temp_dir)
|
||||
|
||||
|
||||
bookmarks = [{"name": "Test", "url": "test://example"}]
|
||||
bookmarks_path = storage._storage_dir / 'bookmarks.json'
|
||||
|
||||
with open(bookmarks_path, 'w', encoding='utf-8') as f:
|
||||
bookmarks_path = storage._storage_dir / "bookmarks.json"
|
||||
|
||||
with open(bookmarks_path, "w", encoding="utf-8") as f:
|
||||
json.dump(bookmarks, f)
|
||||
|
||||
|
||||
loaded_bookmarks = storage.load_bookmarks()
|
||||
assert loaded_bookmarks == bookmarks
|
||||
|
||||
@@ -217,7 +264,7 @@ class TestStorageManager:
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
storage = StorageManager()
|
||||
storage._storage_dir = Path(temp_dir)
|
||||
|
||||
|
||||
loaded_bookmarks = storage.load_bookmarks()
|
||||
assert loaded_bookmarks == []
|
||||
|
||||
@@ -226,15 +273,15 @@ class TestStorageManager:
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
storage = StorageManager()
|
||||
storage._storage_dir = Path(temp_dir)
|
||||
|
||||
|
||||
history = [{"url": "test://example", "timestamp": 1234567890}]
|
||||
result = storage.save_history(history)
|
||||
|
||||
|
||||
assert result is True
|
||||
history_path = storage._storage_dir / 'history.json'
|
||||
history_path = storage._storage_dir / "history.json"
|
||||
assert history_path.exists()
|
||||
|
||||
with open(history_path, 'r', encoding='utf-8') as f:
|
||||
|
||||
with open(history_path, "r", encoding="utf-8") as f:
|
||||
loaded_history = json.load(f)
|
||||
assert loaded_history == history
|
||||
|
||||
@@ -243,13 +290,13 @@ class TestStorageManager:
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
storage = StorageManager()
|
||||
storage._storage_dir = Path(temp_dir)
|
||||
|
||||
|
||||
history = [{"url": "test://example", "timestamp": 1234567890}]
|
||||
history_path = storage._storage_dir / 'history.json'
|
||||
|
||||
with open(history_path, 'w', encoding='utf-8') as f:
|
||||
history_path = storage._storage_dir / "history.json"
|
||||
|
||||
with open(history_path, "w", encoding="utf-8") as f:
|
||||
json.dump(history, f)
|
||||
|
||||
|
||||
loaded_history = storage.load_history()
|
||||
assert loaded_history == history
|
||||
|
||||
@@ -258,33 +305,36 @@ class TestStorageManager:
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
mock_page = Mock()
|
||||
mock_page.client_storage = Mock()
|
||||
|
||||
|
||||
storage = StorageManager(mock_page)
|
||||
storage._storage_dir = Path(temp_dir)
|
||||
|
||||
|
||||
info = storage.get_storage_info()
|
||||
|
||||
assert 'storage_dir' in info
|
||||
assert 'config_path' in info
|
||||
assert 'reticulum_config_path' in info
|
||||
assert 'storage_dir_exists' in info
|
||||
assert 'storage_dir_writable' in info
|
||||
assert 'has_client_storage' in info
|
||||
|
||||
assert info['storage_dir'] == str(Path(temp_dir))
|
||||
assert info['storage_dir_exists'] is True
|
||||
assert info['has_client_storage'] is True
|
||||
|
||||
assert "storage_dir" in info
|
||||
assert "config_path" in info
|
||||
assert "reticulum_config_path" in info
|
||||
assert "storage_dir_exists" in info
|
||||
assert "storage_dir_writable" in info
|
||||
assert "has_client_storage" in info
|
||||
|
||||
assert info["storage_dir"] == str(Path(temp_dir))
|
||||
assert info["storage_dir_exists"] is True
|
||||
assert info["has_client_storage"] is True
|
||||
|
||||
def test_storage_directory_fallback(self):
|
||||
"""Test fallback to temp directory when storage creation fails."""
|
||||
with patch.object(StorageManager, '_get_storage_directory') as mock_get_dir:
|
||||
mock_get_dir.return_value = Path('/nonexistent/path')
|
||||
|
||||
with patch('pathlib.Path.mkdir', side_effect=[PermissionError("Access denied"), None]):
|
||||
with patch('tempfile.gettempdir', return_value='/tmp'):
|
||||
with patch.object(StorageManager, "_get_storage_directory") as mock_get_dir:
|
||||
mock_get_dir.return_value = Path("/nonexistent/path")
|
||||
|
||||
with patch(
|
||||
"pathlib.Path.mkdir",
|
||||
side_effect=[PermissionError("Access denied"), None],
|
||||
):
|
||||
with patch("tempfile.gettempdir", return_value="/tmp"):
|
||||
storage = StorageManager()
|
||||
|
||||
expected_fallback = Path('/tmp') / 'ren_browser'
|
||||
|
||||
expected_fallback = Path("/tmp") / "ren_browser"
|
||||
assert storage._storage_dir == expected_fallback
|
||||
|
||||
|
||||
@@ -293,28 +343,28 @@ class TestStorageGlobalFunctions:
|
||||
|
||||
def test_get_storage_manager_singleton(self):
|
||||
"""Test that get_storage_manager returns the same instance."""
|
||||
with patch('ren_browser.storage.storage._storage_manager', None):
|
||||
with patch("ren_browser.storage.storage._storage_manager", None):
|
||||
storage1 = get_storage_manager()
|
||||
storage2 = get_storage_manager()
|
||||
|
||||
|
||||
assert storage1 is storage2
|
||||
|
||||
def test_get_storage_manager_with_page(self):
|
||||
"""Test get_storage_manager with page parameter."""
|
||||
mock_page = Mock()
|
||||
|
||||
with patch('ren_browser.storage.storage._storage_manager', None):
|
||||
|
||||
with patch("ren_browser.storage.storage._storage_manager", None):
|
||||
storage = get_storage_manager(mock_page)
|
||||
|
||||
|
||||
assert storage.page == mock_page
|
||||
|
||||
def test_initialize_storage(self):
|
||||
"""Test initialize_storage function."""
|
||||
mock_page = Mock()
|
||||
|
||||
with patch('ren_browser.storage.storage._storage_manager', None):
|
||||
|
||||
with patch("ren_browser.storage.storage._storage_manager", None):
|
||||
storage = initialize_storage(mock_page)
|
||||
|
||||
|
||||
assert storage.page == mock_page
|
||||
assert get_storage_manager() is storage
|
||||
|
||||
@@ -327,11 +377,18 @@ class TestStorageManagerEdgeCases:
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
storage = StorageManager()
|
||||
storage._storage_dir = Path(temp_dir)
|
||||
|
||||
|
||||
# Mock the reticulum config path to use temp dir
|
||||
with patch.object(storage, 'get_reticulum_config_path', return_value=Path(temp_dir) / "reticulum"):
|
||||
with patch.object(
|
||||
storage,
|
||||
"get_reticulum_config_path",
|
||||
return_value=Path(temp_dir) / "reticulum",
|
||||
):
|
||||
# Test with content that might cause encoding issues
|
||||
with patch('pathlib.Path.write_text', side_effect=UnicodeEncodeError('utf-8', '', 0, 1, 'error')):
|
||||
with patch(
|
||||
"pathlib.Path.write_text",
|
||||
side_effect=UnicodeEncodeError("utf-8", "", 0, 1, "error"),
|
||||
):
|
||||
result = storage.save_config("test content")
|
||||
# Should still succeed due to fallback
|
||||
assert result is False
|
||||
@@ -341,13 +398,17 @@ class TestStorageManagerEdgeCases:
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
storage = StorageManager()
|
||||
storage._storage_dir = Path(temp_dir)
|
||||
|
||||
|
||||
# Create a config file with invalid encoding
|
||||
config_path = storage.get_config_path()
|
||||
config_path.write_bytes(b'\xff\xfe invalid utf-8')
|
||||
|
||||
config_path.write_bytes(b"\xff\xfe invalid utf-8")
|
||||
|
||||
# Mock the reticulum config path to also be in temp dir
|
||||
with patch.object(storage, 'get_reticulum_config_path', return_value=Path(temp_dir) / "reticulum"):
|
||||
with patch.object(
|
||||
storage,
|
||||
"get_reticulum_config_path",
|
||||
return_value=Path(temp_dir) / "reticulum",
|
||||
):
|
||||
# Should return empty string when encoding fails
|
||||
config = storage.load_config()
|
||||
assert config == ""
|
||||
@@ -355,9 +416,11 @@ class TestStorageManagerEdgeCases:
|
||||
def test_is_writable_permission_denied(self):
|
||||
"""Test _is_writable when permission is denied."""
|
||||
storage = StorageManager()
|
||||
|
||||
with patch('pathlib.Path.write_text', side_effect=PermissionError("Access denied")):
|
||||
test_path = Path('/mock/path')
|
||||
|
||||
with patch(
|
||||
"pathlib.Path.write_text", side_effect=PermissionError("Access denied")
|
||||
):
|
||||
test_path = Path("/mock/path")
|
||||
result = storage._is_writable(test_path)
|
||||
assert result is False
|
||||
|
||||
@@ -366,6 +429,6 @@ class TestStorageManagerEdgeCases:
|
||||
with tempfile.TemporaryDirectory() as temp_dir:
|
||||
storage = StorageManager()
|
||||
test_path = Path(temp_dir)
|
||||
|
||||
|
||||
result = storage._is_writable(test_path)
|
||||
assert result is True
|
||||
|
||||
@@ -13,17 +13,20 @@ class TestTabsManager:
|
||||
@pytest.fixture
|
||||
def tabs_manager(self, mock_page):
|
||||
"""Create a TabsManager instance for testing."""
|
||||
with patch("ren_browser.app.RENDERER", "plaintext"), \
|
||||
patch("ren_browser.renderer.plaintext.render_plaintext") as mock_render:
|
||||
|
||||
mock_page.width = 800 # Simulate page width for adaptive logic
|
||||
with (
|
||||
patch("ren_browser.app.RENDERER", "plaintext"),
|
||||
patch("ren_browser.renderer.plaintext.render_plaintext") as mock_render,
|
||||
):
|
||||
mock_render.return_value = Mock(spec=ft.Text)
|
||||
return TabsManager(mock_page)
|
||||
|
||||
def test_tabs_manager_init(self, mock_page):
|
||||
"""Test TabsManager initialization."""
|
||||
with patch("ren_browser.app.RENDERER", "plaintext"), \
|
||||
patch("ren_browser.renderer.plaintext.render_plaintext") as mock_render:
|
||||
|
||||
with (
|
||||
patch("ren_browser.app.RENDERER", "plaintext"),
|
||||
patch("ren_browser.renderer.plaintext.render_plaintext") as mock_render,
|
||||
):
|
||||
mock_render.return_value = Mock(spec=ft.Text)
|
||||
manager = TabsManager(mock_page)
|
||||
|
||||
@@ -32,6 +35,8 @@ class TestTabsManager:
|
||||
assert len(manager.manager.tabs) == 1
|
||||
assert manager.manager.index == 0
|
||||
assert isinstance(manager.tab_bar, ft.Row)
|
||||
assert manager.tab_bar.scroll is None
|
||||
assert manager.overflow_menu is None
|
||||
assert isinstance(manager.content_container, ft.Container)
|
||||
|
||||
def test_tabs_manager_init_micron_renderer(self, mock_page):
|
||||
@@ -55,9 +60,10 @@ class TestTabsManager:
|
||||
|
||||
def test_on_add_click(self, tabs_manager):
|
||||
"""Test adding a new tab via button click."""
|
||||
with patch("ren_browser.app.RENDERER", "plaintext"), \
|
||||
patch("ren_browser.renderer.plaintext.render_plaintext") as mock_render:
|
||||
|
||||
with (
|
||||
patch("ren_browser.app.RENDERER", "plaintext"),
|
||||
patch("ren_browser.renderer.plaintext.render_plaintext") as mock_render,
|
||||
):
|
||||
mock_render.return_value = Mock(spec=ft.Text)
|
||||
initial_count = len(tabs_manager.manager.tabs)
|
||||
|
||||
@@ -147,7 +153,7 @@ class TestTabsManager:
|
||||
"""Test that tab bar has correct controls."""
|
||||
controls = tabs_manager.tab_bar.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 isinstance(controls[-2], ft.IconButton) # Add button
|
||||
assert isinstance(controls[-1], ft.IconButton) # Close button
|
||||
@@ -198,7 +204,7 @@ class TestTabsManager:
|
||||
"""Test management of multiple tabs."""
|
||||
# Add several tabs
|
||||
for i in range(3):
|
||||
tabs_manager._add_tab_internal(f"Tab {i+2}", Mock())
|
||||
tabs_manager._add_tab_internal(f"Tab {i + 2}", Mock())
|
||||
|
||||
assert len(tabs_manager.manager.tabs) == 4
|
||||
|
||||
@@ -220,7 +226,36 @@ class TestTabsManager:
|
||||
tabs_manager._add_tab_internal("Tab 3", content2)
|
||||
|
||||
tabs_manager.select_tab(1)
|
||||
assert tabs_manager.content_container.content == tabs_manager.manager.tabs[1]["content"]
|
||||
assert (
|
||||
tabs_manager.content_container.content
|
||||
== tabs_manager.manager.tabs[1]["content"]
|
||||
)
|
||||
|
||||
tabs_manager.select_tab(2)
|
||||
assert tabs_manager.content_container.content == tabs_manager.manager.tabs[2]["content"]
|
||||
assert (
|
||||
tabs_manager.content_container.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.controls if isinstance(c, ft.Container) and c.visible)
|
||||
assert visible_tabs_small < 11
|
||||
|
||||
# Simulate a larger screen, expecting all tabs to be visible
|
||||
tabs_manager.page.width = 1600
|
||||
tabs_manager._update_tab_visibility()
|
||||
visible_tabs_large = sum(1 for c in tabs_manager.tab_bar.controls if isinstance(c, ft.Container) and c.visible)
|
||||
|
||||
assert visible_tabs_large == 11
|
||||
assert tabs_manager.overflow_menu is None
|
||||
|
||||
@@ -28,7 +28,9 @@ class TestBuildUI:
|
||||
@patch("ren_browser.pages.page_request.PageFetcher")
|
||||
@patch("ren_browser.tabs.tabs.TabsManager")
|
||||
@patch("ren_browser.controls.shortcuts.Shortcuts")
|
||||
def test_build_ui_appbar_setup(self, mock_shortcuts, mock_tabs, mock_fetcher, mock_announce_service, mock_page):
|
||||
def test_build_ui_appbar_setup(
|
||||
self, mock_shortcuts, mock_tabs, mock_fetcher, mock_announce_service, mock_page
|
||||
):
|
||||
"""Test that build_ui sets up the app bar correctly."""
|
||||
mock_tab_manager = Mock()
|
||||
mock_tabs.return_value = mock_tab_manager
|
||||
@@ -48,7 +50,9 @@ class TestBuildUI:
|
||||
@patch("ren_browser.pages.page_request.PageFetcher")
|
||||
@patch("ren_browser.tabs.tabs.TabsManager")
|
||||
@patch("ren_browser.controls.shortcuts.Shortcuts")
|
||||
def test_build_ui_drawer_setup(self, mock_shortcuts, mock_tabs, mock_fetcher, mock_announce_service, mock_page):
|
||||
def test_build_ui_drawer_setup(
|
||||
self, mock_shortcuts, mock_tabs, mock_fetcher, mock_announce_service, mock_page
|
||||
):
|
||||
"""Test that build_ui sets up the drawer correctly."""
|
||||
mock_tab_manager = Mock()
|
||||
mock_tabs.return_value = mock_tab_manager
|
||||
@@ -116,9 +120,10 @@ class TestOpenSettingsTab:
|
||||
mock_tab_manager._add_tab_internal = Mock()
|
||||
mock_tab_manager.select_tab = Mock()
|
||||
|
||||
with patch("pathlib.Path.read_text", return_value="config"), \
|
||||
patch("pathlib.Path.write_text"):
|
||||
|
||||
with (
|
||||
patch("pathlib.Path.read_text", return_value="config"),
|
||||
patch("pathlib.Path.write_text"),
|
||||
):
|
||||
open_settings_tab(mock_page, mock_tab_manager)
|
||||
|
||||
# Get the settings content that was added
|
||||
@@ -129,7 +134,10 @@ class TestOpenSettingsTab:
|
||||
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 and Restart":
|
||||
if (
|
||||
hasattr(sub_control, "text")
|
||||
and sub_control.text == "Save Config"
|
||||
):
|
||||
save_btn = sub_control
|
||||
break
|
||||
|
||||
@@ -142,7 +150,10 @@ class TestOpenSettingsTab:
|
||||
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):
|
||||
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]
|
||||
@@ -155,10 +166,14 @@ class TestOpenSettingsTab:
|
||||
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), \
|
||||
patch("ren_browser.logs.ERROR_LOGS", ["Error 1", "Error 2"]), \
|
||||
patch("ren_browser.logs.RET_LOGS", ["RNS log 1", "RNS log 2"]):
|
||||
|
||||
with (
|
||||
patch(
|
||||
"ren_browser.ui.settings.get_storage_manager",
|
||||
return_value=mock_storage_manager,
|
||||
),
|
||||
patch("ren_browser.logs.ERROR_LOGS", ["Error 1", "Error 2"]),
|
||||
patch("ren_browser.logs.RET_LOGS", ["RNS log 1", "RNS log 2"]),
|
||||
):
|
||||
open_settings_tab(mock_page, mock_tab_manager)
|
||||
|
||||
mock_tab_manager._add_tab_internal.assert_called_once()
|
||||
|
||||
Reference in New Issue
Block a user