Compare commits
126 Commits
v0.1.1
...
renovate/h
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
333709aad7 | ||
|
a32215f434
|
|||
|
59e016815b
|
|||
|
6b8ce85ea2
|
|||
|
62c280daf2
|
|||
|
e817238fb9
|
|||
|
37bc4948d1
|
|||
| 8080f2855f | |||
|
|
95fa215162 | ||
|
|
cd08064678 | ||
|
|
01b3a54abf | ||
|
|
cb41f89cc9 | ||
|
|
d06a93995e | ||
|
|
dbfe2fd35c | ||
|
|
07754bc9fa | ||
|
|
898919e160 | ||
|
761c1b356c
|
|||
|
|
54b47c8eaf | ||
|
|
55343b7be2 | ||
|
9c6da64cbe
|
|||
|
d0a484f692
|
|||
|
|
31dd0828a2 | ||
|
954f6ecd36
|
|||
|
85c8785502
|
|||
|
112348d862
|
|||
|
4f8f2786ab
|
|||
|
3e6e078367
|
|||
|
73c9d12f26
|
|||
|
fc50bc6fb5
|
|||
|
8f1d5ee02a
|
|||
|
86f0a687d2
|
|||
|
694ab011ec
|
|||
|
53d74a3732
|
|||
|
30f050c8d4
|
|||
|
070157737b
|
|||
|
8538d9feb3
|
|||
|
3438b271a5
|
|||
|
|
d6228d6d63 | ||
|
ccf954681b
|
|||
|
4ec44900cf
|
|||
|
d4099fb9a2
|
|||
|
1571b315b2
|
|||
|
71bd49bd7d
|
|||
|
382413dc08
|
|||
|
0621facc7d
|
|||
|
50cbfed5fa
|
|||
|
36d9a3350b
|
|||
|
515a9d9dbf
|
|||
|
3c27b4f9b8
|
|||
|
851c8c05d4
|
|||
|
8002a75e26
|
|||
|
06e6b55ecc
|
|||
|
48e47bd0bd
|
|||
|
9c074a0333
|
|||
|
f2314f862c
|
|||
|
6e57536650
|
|||
|
5fd7551874
|
|||
|
62d592c4d0
|
|||
|
8af2a9abbb
|
|||
|
64ca8bd4d2
|
|||
|
f1d025bd0e
|
|||
|
087ff563a2
|
|||
|
882dacf2bb
|
|||
|
a2efdb136a
|
|||
|
001613b4fa
|
|||
|
74564d0ef2
|
|||
|
81142ad194
|
|||
|
fee1d2e2d6
|
|||
|
7c93fdb71d
|
|||
| 9e435eeebc | |||
| 5dfcc1f2ce | |||
| 2def60b457 | |||
| f708ad4ee1 | |||
| f7568d81aa | |||
| 251f9bacef | |||
| 07892dbfee | |||
| 54e6849968 | |||
| ea27c380cb | |||
|
|
a338be85e1 | ||
|
|
e31cb3418b | ||
|
|
798725dca6 | ||
|
|
6f393497f0 | ||
|
|
14b5aabf2b | ||
| fb36907447 | |||
| 62fde2617b | |||
| 9f5ea23eb7 | |||
| 19fad61706 | |||
| c900cf38c9 | |||
| 014ebc25c6 | |||
|
|
d5e9308fb5 | ||
|
|
7d5e891261 | ||
|
|
c382ed790f | ||
| cb72e57da9 | |||
|
|
aaf5ad23e2 | ||
|
|
ce1b1dad7d | ||
|
|
67ebc7e556 | ||
|
|
b31fb748b8 | ||
|
|
eb27326763 | ||
|
|
f40d5a51ae | ||
|
|
4aa83a2dfb | ||
|
|
a8b09611a1 | ||
|
|
d0dd9e88c4 | ||
|
|
09d84c2533 | ||
|
|
06ab592cb9 | ||
|
|
843c3a1a56 | ||
|
|
37ac95753c | ||
|
|
a493c57ad2 | ||
|
|
698bfb2e81 | ||
|
|
eaf2e544c4 | ||
|
|
89f88e24ea | ||
|
|
a47b78c13d | ||
|
|
4831f5261d | ||
|
|
46f90e461f | ||
|
|
a1bbe8bc8a | ||
|
|
7092883834 | ||
|
|
74f5174254 | ||
|
|
f9699c060a | ||
|
|
a3ccd49439 | ||
|
|
ece0473beb | ||
|
|
89065f6e0a | ||
|
|
e873d8e754 | ||
|
|
5561205b3e | ||
|
|
d6601adb38 | ||
|
|
98c71a888e | ||
|
|
65bd70c05a | ||
|
|
b77b73576f |
7
.deepsource.toml
Normal file
7
.deepsource.toml
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
version = 1
|
||||||
|
|
||||||
|
[[analyzers]]
|
||||||
|
name = "python"
|
||||||
|
|
||||||
|
[analyzers.meta]
|
||||||
|
runtime_version = "3.x.x"
|
||||||
95
.gitea/workflows/docker.yml
Normal file
95
.gitea/workflows/docker.yml
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
name: Build and Publish Docker Image
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
push:
|
||||||
|
tags: [ 'v*' ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ main, master ]
|
||||||
|
|
||||||
|
env:
|
||||||
|
REGISTRY: git.quad4.io
|
||||||
|
IMAGE_NAME: RNS-Things/rns-page-node
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
outputs:
|
||||||
|
image_digest: ${{ steps.build.outputs.digest }}
|
||||||
|
image_tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: https://git.quad4.io/actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: https://git.quad4.io/actions/setup-qemu-action@3a1695b1353f9f8868722ffaafc1f164ef35fa5e # v3
|
||||||
|
with:
|
||||||
|
platforms: amd64,arm64
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: https://git.quad4.io/actions/setup-buildx-action@7fbd262f0ca05b45700d8eaaf71f40837d036cc7 # v3
|
||||||
|
|
||||||
|
- name: Log in to the Container registry
|
||||||
|
uses: https://git.quad4.io/actions/login-action@bb91d7e20cfedb030a44164cb558bba899e1010a # v3
|
||||||
|
with:
|
||||||
|
registry: ${{ env.REGISTRY }}
|
||||||
|
username: ${{ secrets.REGISTRY_USERNAME }}
|
||||||
|
password: ${{ secrets.REGISTRY_PASSWORD }}
|
||||||
|
|
||||||
|
- name: Extract metadata (tags, labels) for Docker
|
||||||
|
id: meta
|
||||||
|
uses: https://git.quad4.io/actions/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5
|
||||||
|
with:
|
||||||
|
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
||||||
|
tags: |
|
||||||
|
type=raw,value=latest,enable={{is_default_branch}}
|
||||||
|
type=ref,event=branch,prefix=,suffix=,enable={{is_default_branch}}
|
||||||
|
type=ref,event=pr
|
||||||
|
type=semver,pattern={{version}}
|
||||||
|
type=semver,pattern={{major}}.{{minor}}
|
||||||
|
type=sha,format=short
|
||||||
|
|
||||||
|
- name: Build and push Docker image
|
||||||
|
id: build
|
||||||
|
uses: https://git.quad4.io/actions/build-push-action@dc0c2d97df39a6939d9db7d572445529e2365ec6 # v6
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: ./docker/Dockerfile
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
|
push: ${{ startsWith(github.ref, 'refs/tags/v') }}
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
build-args: |
|
||||||
|
BUILD_DATE=${{ github.event.head_commit.timestamp }}
|
||||||
|
VCS_REF=${{ github.sha }}
|
||||||
|
VERSION=${{ steps.meta.outputs.version }}
|
||||||
|
|
||||||
|
- name: Extract metadata (tags, labels) for Docker (rootless)
|
||||||
|
id: meta_rootless
|
||||||
|
uses: https://git.quad4.io/actions/metadata-action@c299e40c65443455700f0fdfc63efafe5b349051 # v5
|
||||||
|
with:
|
||||||
|
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-rootless
|
||||||
|
tags: |
|
||||||
|
type=raw,value=latest-rootless,enable={{is_default_branch}}
|
||||||
|
type=ref,event=branch,prefix=,suffix=-rootless,enable={{is_default_branch}}
|
||||||
|
type=semver,pattern={{version}},suffix=-rootless
|
||||||
|
type=semver,pattern={{major}}.{{minor}},suffix=-rootless
|
||||||
|
type=sha,format=short,suffix=-rootless
|
||||||
|
|
||||||
|
- name: Build and push rootless Docker image
|
||||||
|
uses: https://git.quad4.io/actions/build-push-action@dc0c2d97df39a6939d9db7d572445529e2365ec6 # v6
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: ./docker/Dockerfile.rootless
|
||||||
|
platforms: linux/amd64,linux/arm64
|
||||||
|
push: ${{ startsWith(github.ref, 'refs/tags/v') }}
|
||||||
|
tags: ${{ steps.meta_rootless.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta_rootless.outputs.labels }}
|
||||||
|
build-args: |
|
||||||
|
BUILD_DATE=${{ github.event.head_commit.timestamp }}
|
||||||
|
VCS_REF=${{ github.sha }}
|
||||||
|
VERSION=${{ steps.meta_rootless.outputs.version }}
|
||||||
68
.gitea/workflows/publish.yml
Normal file
68
.gitea/workflows/publish.yml
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
name: Create Release
|
||||||
|
|
||||||
|
# This workflow creates releases:
|
||||||
|
# 1. Build packages
|
||||||
|
# 2. Create Gitea release with all artifacts atomically
|
||||||
|
# This ensures releases cannot be modified once published.
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
version:
|
||||||
|
description: 'Version to release (e.g., 0.6.8)'
|
||||||
|
required: true
|
||||||
|
type: string
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Build distribution 📦
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: https://git.quad4.io/actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||||
|
with:
|
||||||
|
persist-credentials: false
|
||||||
|
- name: Set up Python
|
||||||
|
uses: https://git.quad4.io/actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v5
|
||||||
|
with:
|
||||||
|
python-version: "3.13"
|
||||||
|
- name: Install pypa/build
|
||||||
|
run: python3 -m pip install build --user
|
||||||
|
- name: Build a binary wheel and a source tarball
|
||||||
|
run: python3 -m build
|
||||||
|
- name: Store the distribution packages
|
||||||
|
uses: https://git.quad4.io/actions/upload-artifact@8689daa8608e46baf41e4786cb83fbc0dea972cd # v4
|
||||||
|
with:
|
||||||
|
name: python-package-distributions
|
||||||
|
path: dist/
|
||||||
|
|
||||||
|
gitea-release:
|
||||||
|
name: Create Gitea Release
|
||||||
|
needs:
|
||||||
|
- build
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Download all the dists
|
||||||
|
uses: https://git.quad4.io/actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5
|
||||||
|
with:
|
||||||
|
name: python-package-distributions
|
||||||
|
path: dist/
|
||||||
|
- name: Create Gitea Release with artifacts
|
||||||
|
uses: https://git.quad4.io/actions/gitea-release-action@4875285c0950474efb7ca2df55233c51333eeb74
|
||||||
|
with:
|
||||||
|
tag_name: ${{ github.ref_name }}
|
||||||
|
name: Release ${{ github.ref_name }}
|
||||||
|
files: |
|
||||||
|
dist/*.tar.gz
|
||||||
|
dist/*.whl
|
||||||
22
.gitea/workflows/safety.yml
Normal file
22
.gitea/workflows/safety.yml
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
name: Safety
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ main ]
|
||||||
|
schedule:
|
||||||
|
- cron: '0 0 * * 0' # weekly
|
||||||
|
jobs:
|
||||||
|
security:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: https://git.quad4.io/actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
|
||||||
|
- name: Set up Python
|
||||||
|
uses: https://git.quad4.io/actions/setup-python@83679a892e2d95755f2dac6acb0bfd1e9ac5d548 # v5
|
||||||
|
with:
|
||||||
|
python-version: '3.10'
|
||||||
|
- name: Install dependencies
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
pip install .
|
||||||
|
- name: Run pip-audit
|
||||||
|
uses: https://git.quad4.io/actions/gh-action-pip-audit@66a6ee35b1b25f89c6bdc9f7c11284f08061823a # v1.1.0
|
||||||
68
.github/workflows/docker.yml
vendored
68
.github/workflows/docker.yml
vendored
@@ -1,68 +0,0 @@
|
|||||||
name: Build and Publish Docker Image
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [ main ]
|
|
||||||
tags: [ 'v*' ]
|
|
||||||
pull_request:
|
|
||||||
branches: [ main ]
|
|
||||||
|
|
||||||
env:
|
|
||||||
REGISTRY: ghcr.io
|
|
||||||
IMAGE_NAME: ${{ github.repository }}
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
packages: write
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v3
|
|
||||||
|
|
||||||
- name: Log in to the Container registry
|
|
||||||
uses: docker/login-action@v3
|
|
||||||
with:
|
|
||||||
registry: ${{ env.REGISTRY }}
|
|
||||||
username: ${{ github.actor }}
|
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Extract metadata (tags, labels) for Docker
|
|
||||||
id: meta
|
|
||||||
uses: docker/metadata-action@v5
|
|
||||||
with:
|
|
||||||
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
|
|
||||||
tags: |
|
|
||||||
type=raw,value=latest,enable={{is_default_branch}}
|
|
||||||
type=ref,event=branch,prefix=,suffix=,enable={{is_default_branch}}
|
|
||||||
type=ref,event=pr
|
|
||||||
type=semver,pattern={{version}}
|
|
||||||
type=semver,pattern={{major}}.{{minor}}
|
|
||||||
type=sha,format=short
|
|
||||||
|
|
||||||
- name: Build and push Docker image
|
|
||||||
uses: docker/build-push-action@v5
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
file: Dockerfile
|
|
||||||
push: ${{ github.event_name != 'pull_request' }}
|
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
|
||||||
cache-from: type=gha
|
|
||||||
cache-to: type=gha,mode=max
|
|
||||||
|
|
||||||
- name: Build and push rootless Docker image
|
|
||||||
uses: docker/build-push-action@v5
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
file: Dockerfile.rootless
|
|
||||||
push: ${{ github.event_name != 'pull_request' }}
|
|
||||||
tags: ${{ steps.meta.outputs.tags }}-rootless
|
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
|
||||||
cache-from: type=gha
|
|
||||||
cache-to: type=gha,mode=max
|
|
||||||
9
.gitignore
vendored
9
.gitignore
vendored
@@ -3,3 +3,12 @@ node-config/
|
|||||||
files/
|
files/
|
||||||
.ruff_cache/
|
.ruff_cache/
|
||||||
__pycache__/
|
__pycache__/
|
||||||
|
dist/
|
||||||
|
*.egg-info/
|
||||||
|
.ruff_cache/
|
||||||
|
.venv/
|
||||||
|
.env
|
||||||
|
.env.local
|
||||||
|
.env.development.local
|
||||||
|
.env.test.local
|
||||||
|
.env.production.local
|
||||||
17
Dockerfile
17
Dockerfile
@@ -1,17 +0,0 @@
|
|||||||
FROM python:3.13-alpine
|
|
||||||
|
|
||||||
LABEL org.opencontainers.image.source="https://github.com/Sudo-Ivan/rns-page-node"
|
|
||||||
LABEL org.opencontainers.image.description="A simple way to serve pages and files over the Reticulum network."
|
|
||||||
LABEL org.opencontainers.image.licenses="GPL-3.0"
|
|
||||||
LABEL org.opencontainers.image.authors="Sudo-Ivan"
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
COPY requirements.txt ./
|
|
||||||
COPY setup.py ./
|
|
||||||
COPY README.md ./
|
|
||||||
COPY rns_page_node ./rns_page_node
|
|
||||||
|
|
||||||
RUN pip install --upgrade pip setuptools wheel && pip install -r requirements.txt .
|
|
||||||
|
|
||||||
ENTRYPOINT ["rns-page-node"]
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
FROM python:3.13-alpine AS builder
|
|
||||||
|
|
||||||
RUN apk update
|
|
||||||
RUN apk add build-base libffi-dev cargo pkgconfig
|
|
||||||
|
|
||||||
WORKDIR /src
|
|
||||||
|
|
||||||
COPY setup.py ./
|
|
||||||
COPY README.md ./
|
|
||||||
COPY rns_page_node ./rns_page_node
|
|
||||||
|
|
||||||
RUN pip install --upgrade pip setuptools wheel
|
|
||||||
|
|
||||||
RUN pip wheel . --no-deps --wheel-dir /src/dist
|
|
||||||
|
|
||||||
FROM scratch AS dist
|
|
||||||
|
|
||||||
COPY --from=builder /src/dist .
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
FROM python:3.13-alpine
|
|
||||||
|
|
||||||
LABEL org.opencontainers.image.source="https://github.com/Sudo-Ivan/rns-page-node"
|
|
||||||
LABEL org.opencontainers.image.description="A simple way to serve pages and files over the Reticulum network."
|
|
||||||
LABEL org.opencontainers.image.licenses="GPL-3.0"
|
|
||||||
LABEL org.opencontainers.image.authors="Sudo-Ivan"
|
|
||||||
|
|
||||||
RUN addgroup -g 1000 app && adduser -D -u 1000 -G app app
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
COPY requirements.txt setup.py README.md ./
|
|
||||||
|
|
||||||
COPY rns_page_node ./rns_page_node
|
|
||||||
|
|
||||||
RUN pip install --upgrade pip setuptools wheel && pip install -r requirements.txt .
|
|
||||||
|
|
||||||
USER app
|
|
||||||
|
|
||||||
ENTRYPOINT ["rns-page-node"]
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
45
Makefile
45
Makefile
@@ -1,17 +1,31 @@
|
|||||||
# Makefile for rns-page-node
|
# Makefile for rns-page-node
|
||||||
|
|
||||||
.PHONY: all build sdist wheel clean install lint format docker-wheels docker-build docker-run docker-build-rootless docker-run-rootless help
|
# Extract version from pyproject.toml
|
||||||
|
VERSION := $(shell grep "^version =" pyproject.toml | cut -d '"' -f 2)
|
||||||
|
VCS_REF := $(shell git rev-parse --short HEAD 2>/dev/null || echo "unknown")
|
||||||
|
BUILD_DATE := $(shell date -u +"%Y-%m-%dT%H:%M:%SZ")
|
||||||
|
|
||||||
|
# Detect if docker buildx is available
|
||||||
|
DOCKER_BUILD := $(shell docker buildx version >/dev/null 2>&1 && echo "docker buildx build" || echo "docker build")
|
||||||
|
DOCKER_BUILD_LOAD := $(shell docker buildx version >/dev/null 2>&1 && echo "docker buildx build --load" || echo "docker build")
|
||||||
|
|
||||||
|
# Build arguments for Docker
|
||||||
|
DOCKER_BUILD_ARGS := --build-arg VERSION=$(VERSION) \
|
||||||
|
--build-arg VCS_REF=$(VCS_REF) \
|
||||||
|
--build-arg BUILD_DATE=$(BUILD_DATE)
|
||||||
|
|
||||||
|
.PHONY: all build sdist wheel clean install lint format docker-wheels docker-build docker-run docker-build-rootless docker-run-rootless help test docker-test
|
||||||
|
|
||||||
all: build
|
all: build
|
||||||
|
|
||||||
build: clean
|
build: clean
|
||||||
python3 setup.py sdist bdist_wheel
|
python3 -m build
|
||||||
|
|
||||||
sdist:
|
sdist:
|
||||||
python3 setup.py sdist
|
python3 -m build --sdist
|
||||||
|
|
||||||
wheel:
|
wheel:
|
||||||
python3 setup.py bdist_wheel
|
python3 -m build --wheel
|
||||||
|
|
||||||
clean:
|
clean:
|
||||||
rm -rf build dist *.egg-info
|
rm -rf build dist *.egg-info
|
||||||
@@ -26,20 +40,20 @@ format:
|
|||||||
ruff check --fix .
|
ruff check --fix .
|
||||||
|
|
||||||
docker-wheels:
|
docker-wheels:
|
||||||
docker build --target builder -f Dockerfile.build -t rns-page-node-builder .
|
$(DOCKER_BUILD) --target builder -f docker/Dockerfile.build -t rns-page-node-builder .
|
||||||
docker create --name builder-container rns-page-node-builder true
|
docker create --name builder-container rns-page-node-builder true
|
||||||
docker cp builder-container:/src/dist ./dist
|
docker cp builder-container:/src/dist ./dist
|
||||||
docker rm builder-container
|
docker rm builder-container
|
||||||
|
|
||||||
docker-build:
|
docker-build:
|
||||||
docker build -f Dockerfile -t rns-page-node:latest .
|
$(DOCKER_BUILD_LOAD) $(DOCKER_BUILD_ARGS) $(BUILD_ARGS) -f docker/Dockerfile -t git.quad4.io/rns-things/rns-page-node:latest -t git.quad4.io/rns-things/rns-page-node:$(VERSION) .
|
||||||
|
|
||||||
docker-run:
|
docker-run:
|
||||||
docker run --rm -it \
|
docker run --rm -it \
|
||||||
-v ./pages:/app/pages \
|
-v ./pages:/app/pages \
|
||||||
-v ./files:/app/files \
|
-v ./files:/app/files \
|
||||||
-v ./node-config:/app/node-config \
|
-v ./node-config:/app/node-config \
|
||||||
rns-page-node:latest \
|
git.quad4.io/rns-things/rns-page-node:latest \
|
||||||
--node-name "Page Node" \
|
--node-name "Page Node" \
|
||||||
--pages-dir /app/pages \
|
--pages-dir /app/pages \
|
||||||
--files-dir /app/files \
|
--files-dir /app/files \
|
||||||
@@ -47,20 +61,27 @@ docker-run:
|
|||||||
--announce-interval 360
|
--announce-interval 360
|
||||||
|
|
||||||
docker-build-rootless:
|
docker-build-rootless:
|
||||||
docker build -f Dockerfile.rootless -t rns-page-node-rootless:latest .
|
$(DOCKER_BUILD_LOAD) $(DOCKER_BUILD_ARGS) $(BUILD_ARGS) -f docker/Dockerfile.rootless -t git.quad4.io/rns-things/rns-page-node:latest-rootless -t git.quad4.io/rns-things/rns-page-node:$(VERSION)-rootless .
|
||||||
|
|
||||||
docker-run-rootless:
|
docker-run-rootless:
|
||||||
docker run --rm -it \
|
docker run --rm -it \
|
||||||
-v ./pages:/app/pages \
|
-v ./pages:/app/pages \
|
||||||
-v ./files:/app/files \
|
-v ./files:/app/files \
|
||||||
-v ./node-config:/app/node-config \
|
-v ./node-config:/app/node-config \
|
||||||
rns-page-node-rootless:latest \
|
git.quad4.io/rns-things/rns-page-node:latest-rootless \
|
||||||
--node-name "Page Node" \
|
--node-name "Page Node" \
|
||||||
--pages-dir /app/pages \
|
--pages-dir /app/pages \
|
||||||
--files-dir /app/files \
|
--files-dir /app/files \
|
||||||
--identity-dir /app/node-config \
|
--identity-dir /app/node-config \
|
||||||
--announce-interval 360
|
--announce-interval 360
|
||||||
|
|
||||||
|
test:
|
||||||
|
bash tests/run_tests.sh
|
||||||
|
|
||||||
|
docker-test:
|
||||||
|
$(DOCKER_BUILD_LOAD) -f docker/Dockerfile.tests -t rns-page-node-tests .
|
||||||
|
docker run --rm rns-page-node-tests
|
||||||
|
|
||||||
help:
|
help:
|
||||||
@echo "Makefile commands:"
|
@echo "Makefile commands:"
|
||||||
@echo " all - alias for build"
|
@echo " all - alias for build"
|
||||||
@@ -72,7 +93,9 @@ help:
|
|||||||
@echo " lint - run ruff linter"
|
@echo " lint - run ruff linter"
|
||||||
@echo " format - run ruff --fix"
|
@echo " format - run ruff --fix"
|
||||||
@echo " docker-wheels - build Python wheels in Docker"
|
@echo " docker-wheels - build Python wheels in Docker"
|
||||||
@echo " docker-build - build runtime Docker image"
|
@echo " docker-build - build runtime Docker image (version: $(VERSION))"
|
||||||
@echo " docker-run - run runtime Docker image"
|
@echo " docker-run - run runtime Docker image"
|
||||||
@echo " docker-build-rootless - build rootless runtime Docker image"
|
@echo " docker-build-rootless - build rootless runtime Docker image"
|
||||||
@echo " docker-run-rootless - run rootless runtime Docker image"
|
@echo " docker-run-rootless - run rootless runtime Docker image"
|
||||||
|
@echo " test - run local integration tests"
|
||||||
|
@echo " docker-test - build and run integration tests in Docker"
|
||||||
|
|||||||
116
README.md
116
README.md
@@ -1,31 +1,84 @@
|
|||||||
# RNS Page Node
|
# RNS Page Node
|
||||||
|
|
||||||
|
[Русская](README.ru.md)
|
||||||
|
|
||||||
A simple way to serve pages and files over the [Reticulum network](https://reticulum.network/). Drop-in replacement for [NomadNet](https://github.com/markqvist/NomadNet) nodes that primarily serve pages and files.
|
A simple way to serve pages and files over the [Reticulum network](https://reticulum.network/). Drop-in replacement for [NomadNet](https://github.com/markqvist/NomadNet) nodes that primarily serve pages and files.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Serves pages and files over RNS
|
||||||
|
- Dynamic page support with environment variables
|
||||||
|
- Form data and request parameter parsing
|
||||||
|
|
||||||
|
## To Do
|
||||||
|
|
||||||
|
- [ ] Move to single small and rootless docker image
|
||||||
|
- [ ] Codebase cleanup
|
||||||
|
- [ ] Update PyPI publishing workflow
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pip install git+https://github.com/Sudo-Ivan/rns-page-node.git
|
# Pip
|
||||||
|
# May require --break-system-packages
|
||||||
|
|
||||||
# or
|
pip install rns-page-node
|
||||||
|
|
||||||
pipx install git+https://github.com/Sudo-Ivan/rns-page-node.git
|
# Pipx
|
||||||
|
|
||||||
|
pipx install rns-page-node
|
||||||
|
|
||||||
|
# uv
|
||||||
|
|
||||||
|
uv venv
|
||||||
|
source .venv/bin/activate
|
||||||
|
uv pip install rns-page-node
|
||||||
|
|
||||||
|
# Pipx via Git
|
||||||
|
|
||||||
|
pipx install git+https://git.quad4.io/RNS-Things/rns-page-node.git
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
# will use current directory for pages and files
|
||||||
rns-page-node
|
rns-page-node
|
||||||
```
|
```
|
||||||
|
|
||||||
## Usage
|
or with command-line options:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
rns-page-node --node-name "Page Node" --pages-dir ./pages --files-dir ./files --identity-dir ./node-config --announce-interval 360
|
rns-page-node --node-name "Page Node" --pages-dir ./pages --files-dir ./files --identity-dir ./node-config --announce-interval 360
|
||||||
```
|
```
|
||||||
|
|
||||||
|
or with a config file:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
rns-page-node /path/to/config.conf
|
||||||
|
```
|
||||||
|
|
||||||
|
### Configuration File
|
||||||
|
|
||||||
|
You can use a configuration file to persist settings. See `config.example` for an example.
|
||||||
|
|
||||||
|
Config file format is simple `key=value` pairs:
|
||||||
|
|
||||||
|
```
|
||||||
|
# Comment lines start with #
|
||||||
|
node-name=My Page Node
|
||||||
|
pages-dir=./pages
|
||||||
|
files-dir=./files
|
||||||
|
identity-dir=./node-config
|
||||||
|
announce-interval=360
|
||||||
|
```
|
||||||
|
|
||||||
|
Priority order: Command-line arguments > Config file > Defaults
|
||||||
|
|
||||||
### Docker/Podman
|
### Docker/Podman
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker run -it --rm -v ./pages:/app/pages -v ./files:/app/files -v ./node-config:/app/node-config -v ./config:/app/config ghcr.io/sudo-ivan/rns-page-node:latest
|
docker run -it --rm -v ./pages:/app/pages -v ./files:/app/files -v ./node-config:/app/node-config -v ./config:/root/.reticulum git.quad4.io/rns-things/rns-page-node:latest
|
||||||
```
|
```
|
||||||
|
|
||||||
### Docker/Podman Rootless
|
### Docker/Podman Rootless
|
||||||
@@ -33,30 +86,57 @@ docker run -it --rm -v ./pages:/app/pages -v ./files:/app/files -v ./node-config
|
|||||||
```bash
|
```bash
|
||||||
mkdir -p ./pages ./files ./node-config ./config
|
mkdir -p ./pages ./files ./node-config ./config
|
||||||
chown -R 1000:1000 ./pages ./files ./node-config ./config
|
chown -R 1000:1000 ./pages ./files ./node-config ./config
|
||||||
podman run -it --rm -v ./pages:/app/pages -v ./files:/app/files -v ./node-config:/app/node-config -v ./config:/app/config ghcr.io/sudo-ivan/rns-page-node:latest-rootless
|
podman run -it --rm -v ./pages:/app/pages -v ./files:/app/files -v ./node-config:/app/node-config -v ./config:/app/config git.quad4.io/rns-things/rns-page-node:latest-rootless
|
||||||
```
|
```
|
||||||
|
|
||||||
Mounting volumes are optional, you can also copy pages and files to the container `podman cp` or `docker cp`.
|
Mounting volumes are optional, you can also copy pages and files to the container `podman cp` or `docker cp`.
|
||||||
|
|
||||||
## Page formats
|
## Build
|
||||||
|
|
||||||
- Micron `.mu`
|
```bash
|
||||||
|
make build
|
||||||
|
```
|
||||||
|
|
||||||
|
Build wheels:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make wheel
|
||||||
|
```
|
||||||
|
|
||||||
|
### Build Wheels in Docker
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make docker-wheels
|
||||||
|
```
|
||||||
|
|
||||||
|
## Pages
|
||||||
|
|
||||||
|
Supports dynamic executable pages with full request data parsing. Pages can receive:
|
||||||
|
- Form fields via `field_*` environment variables
|
||||||
|
- Link variables via `var_*` environment variables
|
||||||
|
- Remote identity via `remote_identity` environment variable
|
||||||
|
- Link ID via `link_id` environment variable
|
||||||
|
|
||||||
|
This enables forums, chats, and other interactive applications compatible with NomadNet clients.
|
||||||
|
|
||||||
## Options
|
## Options
|
||||||
|
|
||||||
```
|
```
|
||||||
-c, --config: The path to the Reticulum config file.
|
Positional arguments:
|
||||||
-n, --node-name: The name of the node.
|
node_config Path to rns-page-node config file
|
||||||
-p, --pages-dir: The directory to serve pages from.
|
|
||||||
-f, --files-dir: The directory to serve files from.
|
Optional arguments:
|
||||||
-i, --identity-dir: The directory to persist the node's identity.
|
-c, --config Path to the Reticulum config file
|
||||||
-a, --announce-interval: The interval to announce the node's presence.
|
-n, --node-name Name of the node
|
||||||
|
-p, --pages-dir Directory to serve pages from
|
||||||
|
-f, --files-dir Directory to serve files from
|
||||||
|
-i, --identity-dir Directory to persist the node's identity
|
||||||
|
-a, --announce-interval Interval to announce the node's presence (in minutes, default: 360 = 6 hours)
|
||||||
|
--page-refresh-interval Interval to refresh pages (in seconds, 0 = disabled)
|
||||||
|
--file-refresh-interval Interval to refresh files (in seconds, 0 = disabled)
|
||||||
|
-l, --log-level Logging level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
|
||||||
```
|
```
|
||||||
|
|
||||||
## To-Do
|
|
||||||
|
|
||||||
- [ ] Pypi
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
This project incorporates portions of the [NomadNet](https://github.com/markqvist/NomadNet) codebase, which is licensed under the GNU General Public License v3.0 (GPL-3.0). As a derivative work, this project is also distributed under the terms of the GPL-3.0. See the [LICENSE](LICENSE) file for full license.
|
This project incorporates portions of the [NomadNet](https://github.com/markqvist/NomadNet) codebase, which is licensed under the GNU General Public License v3.0 (GPL-3.0). As a derivative work, this project is also distributed under the terms of the GPL-3.0. See the [LICENSE](LICENSE) file for full license.
|
||||||
|
|||||||
124
README.ru.md
Normal file
124
README.ru.md
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
# RNS Page Node
|
||||||
|
|
||||||
|
[English](README.md)
|
||||||
|
|
||||||
|
Простой способ для раздачи страниц и файлов через сеть [Reticulum](https://reticulum.network/). Прямая замена для узлов [NomadNet](https://github.com/markqvist/NomadNet), которые в основном служат для раздачи страниц и файлов.
|
||||||
|
|
||||||
|
## Особенности
|
||||||
|
|
||||||
|
- Раздача страниц и файлов через RNS
|
||||||
|
- Поддержка динамических страниц с переменными окружения
|
||||||
|
- Разбор данных форм и параметров запросов
|
||||||
|
|
||||||
|
## Установка
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Pip
|
||||||
|
# Может потребоваться --break-system-packages
|
||||||
|
pip install rns-page-node
|
||||||
|
|
||||||
|
# Pipx
|
||||||
|
pipx install rns-page-node
|
||||||
|
|
||||||
|
# uv
|
||||||
|
uv venv
|
||||||
|
source .venv/bin/activate
|
||||||
|
uv pip install rns-page-node
|
||||||
|
|
||||||
|
# Pipx через Git
|
||||||
|
pipx install git+https://git.quad4.io/RNS-Things/rns-page-node.git
|
||||||
|
|
||||||
|
```
|
||||||
|
## Использование
|
||||||
|
```bash
|
||||||
|
# будет использовать текущий каталог для страниц и файлов
|
||||||
|
rns-page-node
|
||||||
|
```
|
||||||
|
|
||||||
|
или с параметрами командной строки:
|
||||||
|
```bash
|
||||||
|
rns-page-node --node-name "Page Node" --pages-dir ./pages --files-dir ./files --identity-dir ./node-config --announce-interval 360
|
||||||
|
```
|
||||||
|
|
||||||
|
или с файлом конфигурации:
|
||||||
|
```bash
|
||||||
|
rns-page-node /путь/к/config.conf
|
||||||
|
```
|
||||||
|
|
||||||
|
### Файл Конфигурации
|
||||||
|
|
||||||
|
Вы можете использовать файл конфигурации для сохранения настроек. См. `config.example` для примера.
|
||||||
|
|
||||||
|
Формат файла конфигурации - простые пары `ключ=значение`:
|
||||||
|
|
||||||
|
```
|
||||||
|
# Строки комментариев начинаются с #
|
||||||
|
node-name=Мой Page Node
|
||||||
|
pages-dir=./pages
|
||||||
|
files-dir=./files
|
||||||
|
identity-dir=./node-config
|
||||||
|
announce-interval=360
|
||||||
|
```
|
||||||
|
|
||||||
|
Порядок приоритета: Аргументы командной строки > Файл конфигурации > Значения по умолчанию
|
||||||
|
|
||||||
|
### Docker/Podman
|
||||||
|
```bash
|
||||||
|
docker run -it --rm -v ./pages:/app/pages -v ./files:/app/files -v ./node-config:/app/node-config -v ./config:/root/.reticulum git.quad4.io/rns-things/rns-page-node:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
### Docker/Podman без root-доступа
|
||||||
|
```bash
|
||||||
|
mkdir -p ./pages ./files ./node-config ./config
|
||||||
|
chown -R 1000:1000 ./pages ./files ./node-config ./config
|
||||||
|
podman run -it --rm -v ./pages:/app/pages -v ./files:/app/files -v ./node-config:/app/node-config -v ./config:/app/config git.quad4.io/rns-things/rns-page-node:latest-rootless
|
||||||
|
```
|
||||||
|
|
||||||
|
Монтирование томов необязательно, вы также можете скопировать страницы и файлы в контейнер с помощью `podman cp` или `docker cp`.
|
||||||
|
|
||||||
|
## Сборка
|
||||||
|
```bash
|
||||||
|
make build
|
||||||
|
```
|
||||||
|
|
||||||
|
Сборка wheels:
|
||||||
|
```bash
|
||||||
|
make wheel
|
||||||
|
```
|
||||||
|
|
||||||
|
### Сборка Wheels в Docker
|
||||||
|
```bash
|
||||||
|
make docker-wheels
|
||||||
|
```
|
||||||
|
|
||||||
|
## Страницы
|
||||||
|
|
||||||
|
Поддержка динамических исполняемых страниц с полным разбором данных запросов. Страницы могут получать:
|
||||||
|
- Поля форм через переменные окружения `field_*`
|
||||||
|
- Переменные ссылок через переменные окружения `var_*`
|
||||||
|
- Удаленную идентификацию через переменную окружения `remote_identity`
|
||||||
|
- ID соединения через переменную окружения `link_id`
|
||||||
|
|
||||||
|
Это позволяет создавать форумы, чаты и другие интерактивные приложения, совместимые с клиентами NomadNet.
|
||||||
|
|
||||||
|
## Параметры
|
||||||
|
|
||||||
|
```
|
||||||
|
Позиционные аргументы:
|
||||||
|
node_config Путь к файлу конфигурации rns-page-node
|
||||||
|
|
||||||
|
Необязательные аргументы:
|
||||||
|
-c, --config Путь к файлу конфигурации Reticulum
|
||||||
|
-n, --node-name Имя узла
|
||||||
|
-p, --pages-dir Каталог для раздачи страниц
|
||||||
|
-f, --files-dir Каталог для раздачи файлов
|
||||||
|
-i, --identity-dir Каталог для сохранения идентификационных данных узла
|
||||||
|
-a, --announce-interval Интервал анонсирования присутствия узла (в минутах, по умолчанию: 360 = 6 часов)
|
||||||
|
--page-refresh-interval Интервал обновления страниц (в секундах, 0 = отключено)
|
||||||
|
--file-refresh-interval Интервал обновления файлов (в секундах, 0 = отключено)
|
||||||
|
-l, --log-level Уровень логирования (DEBUG, INFO, WARNING, ERROR, CRITICAL)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Лицензия
|
||||||
|
|
||||||
|
Этот проект включает части кодовой базы [NomadNet](https://github.com/markqvist/NomadNet), которая лицензирована под GNU General Public License v3.0 (GPL-3.0). Как производная работа, этот проект также распространяется на условиях GPL-3.0. Полный текст лицензии смотрите в файле [LICENSE](LICENSE).
|
||||||
201
Taskfile.yml
Normal file
201
Taskfile.yml
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
version: '3'
|
||||||
|
|
||||||
|
vars:
|
||||||
|
VERSION:
|
||||||
|
sh: grep "^version =" pyproject.toml | cut -d '"' -f 2
|
||||||
|
VCS_REF:
|
||||||
|
sh: git rev-parse --short HEAD 2>/dev/null || echo "unknown"
|
||||||
|
BUILD_DATE:
|
||||||
|
sh: date -u +"%Y-%m-%dT%H:%M:%SZ"
|
||||||
|
DOCKER_BUILD:
|
||||||
|
sh: docker buildx version >/dev/null 2>&1 && echo "docker buildx build" || echo "docker build"
|
||||||
|
DOCKER_BUILD_LOAD:
|
||||||
|
sh: docker buildx version >/dev/null 2>&1 && echo "docker buildx build --load" || echo "docker build"
|
||||||
|
IMAGE_NAME: git.quad4.io/rns-things/rns-page-node
|
||||||
|
|
||||||
|
tasks:
|
||||||
|
default:
|
||||||
|
desc: Show available tasks
|
||||||
|
cmds:
|
||||||
|
- task --list
|
||||||
|
|
||||||
|
build:
|
||||||
|
desc: Clean and build sdist and wheel
|
||||||
|
deps: [clean]
|
||||||
|
cmds:
|
||||||
|
- python3 -m build
|
||||||
|
|
||||||
|
sdist:
|
||||||
|
desc: Build source distribution
|
||||||
|
cmds:
|
||||||
|
- python3 -m build --sdist
|
||||||
|
|
||||||
|
wheel:
|
||||||
|
desc: Build wheel
|
||||||
|
cmds:
|
||||||
|
- python3 -m build --wheel
|
||||||
|
|
||||||
|
clean:
|
||||||
|
desc: Remove build artifacts
|
||||||
|
cmds:
|
||||||
|
- rm -rf build dist *.egg-info
|
||||||
|
|
||||||
|
install:
|
||||||
|
desc: Install built wheel
|
||||||
|
deps: [build]
|
||||||
|
cmds:
|
||||||
|
- pip install dist/*.whl
|
||||||
|
|
||||||
|
install-dev:
|
||||||
|
desc: Install package in development mode
|
||||||
|
cmds:
|
||||||
|
- pip install -e .
|
||||||
|
|
||||||
|
lint:
|
||||||
|
desc: Run ruff linter
|
||||||
|
cmds:
|
||||||
|
- ruff check .
|
||||||
|
|
||||||
|
format:
|
||||||
|
desc: Run ruff formatter with auto-fix
|
||||||
|
cmds:
|
||||||
|
- ruff check --fix .
|
||||||
|
|
||||||
|
check:
|
||||||
|
desc: Run all code quality checks
|
||||||
|
deps: [lint]
|
||||||
|
|
||||||
|
test:
|
||||||
|
desc: Run local integration tests
|
||||||
|
cmds:
|
||||||
|
- bash tests/run_tests.sh
|
||||||
|
|
||||||
|
docker-wheels:
|
||||||
|
desc: Build Python wheels in Docker
|
||||||
|
cmds:
|
||||||
|
- '{{.DOCKER_BUILD}} --target builder -f docker/Dockerfile.build -t rns-page-node-builder .'
|
||||||
|
- docker create --name builder-container rns-page-node-builder true
|
||||||
|
- docker cp builder-container:/src/dist ./dist
|
||||||
|
- docker rm builder-container
|
||||||
|
|
||||||
|
docker-build:
|
||||||
|
desc: Build runtime Docker image
|
||||||
|
cmds:
|
||||||
|
- >
|
||||||
|
{{.DOCKER_BUILD_LOAD}}
|
||||||
|
--build-arg VERSION={{.VERSION}}
|
||||||
|
--build-arg VCS_REF={{.VCS_REF}}
|
||||||
|
--build-arg BUILD_DATE={{.BUILD_DATE}}
|
||||||
|
-f docker/Dockerfile
|
||||||
|
-t {{.IMAGE_NAME}}:latest
|
||||||
|
-t {{.IMAGE_NAME}}:{{.VERSION}}
|
||||||
|
.
|
||||||
|
|
||||||
|
docker-run:
|
||||||
|
desc: Run runtime Docker image
|
||||||
|
cmds:
|
||||||
|
- >
|
||||||
|
docker run --rm -it
|
||||||
|
-v ./pages:/app/pages
|
||||||
|
-v ./files:/app/files
|
||||||
|
-v ./node-config:/app/node-config
|
||||||
|
{{.IMAGE_NAME}}:latest
|
||||||
|
--node-name "Page Node"
|
||||||
|
--pages-dir /app/pages
|
||||||
|
--files-dir /app/files
|
||||||
|
--identity-dir /app/node-config
|
||||||
|
--announce-interval 360
|
||||||
|
|
||||||
|
docker-build-rootless:
|
||||||
|
desc: Build rootless runtime Docker image
|
||||||
|
cmds:
|
||||||
|
- >
|
||||||
|
{{.DOCKER_BUILD_LOAD}}
|
||||||
|
--build-arg VERSION={{.VERSION}}
|
||||||
|
--build-arg VCS_REF={{.VCS_REF}}
|
||||||
|
--build-arg BUILD_DATE={{.BUILD_DATE}}
|
||||||
|
-f docker/Dockerfile.rootless
|
||||||
|
-t {{.IMAGE_NAME}}:latest-rootless
|
||||||
|
-t {{.IMAGE_NAME}}:{{.VERSION}}-rootless
|
||||||
|
.
|
||||||
|
|
||||||
|
docker-run-rootless:
|
||||||
|
desc: Run rootless runtime Docker image
|
||||||
|
cmds:
|
||||||
|
- >
|
||||||
|
docker run --rm -it
|
||||||
|
-v ./pages:/app/pages
|
||||||
|
-v ./files:/app/files
|
||||||
|
-v ./node-config:/app/node-config
|
||||||
|
{{.IMAGE_NAME}}:latest-rootless
|
||||||
|
--node-name "Page Node"
|
||||||
|
--pages-dir /app/pages
|
||||||
|
--files-dir /app/files
|
||||||
|
--identity-dir /app/node-config
|
||||||
|
--announce-interval 360
|
||||||
|
|
||||||
|
docker-test:
|
||||||
|
desc: Build and run integration tests in Docker
|
||||||
|
cmds:
|
||||||
|
- '{{.DOCKER_BUILD_LOAD}} -f docker/Dockerfile.tests -t rns-page-node-tests .'
|
||||||
|
- docker run --rm rns-page-node-tests
|
||||||
|
|
||||||
|
docker-clean:
|
||||||
|
desc: Remove Docker images and containers
|
||||||
|
cmds:
|
||||||
|
- docker rmi {{.IMAGE_NAME}}:latest {{.IMAGE_NAME}}:{{.VERSION}} 2>/dev/null || true
|
||||||
|
- docker rmi {{.IMAGE_NAME}}:latest-rootless {{.IMAGE_NAME}}:{{.VERSION}}-rootless 2>/dev/null || true
|
||||||
|
- docker rmi rns-page-node-builder rns-page-node-tests 2>/dev/null || true
|
||||||
|
|
||||||
|
run:
|
||||||
|
desc: Run rns-page-node locally
|
||||||
|
cmds:
|
||||||
|
- python3 -m rns_page_node.main
|
||||||
|
|
||||||
|
run-dev:
|
||||||
|
desc: Run rns-page-node with development settings
|
||||||
|
cmds:
|
||||||
|
- python3 -m rns_page_node.main --log-level DEBUG
|
||||||
|
|
||||||
|
venv:
|
||||||
|
desc: Create Python virtual environment
|
||||||
|
cmds:
|
||||||
|
- python3 -m venv .venv
|
||||||
|
- 'echo "Virtual environment created. Activate with: source .venv/bin/activate"'
|
||||||
|
|
||||||
|
deps-install:
|
||||||
|
desc: Install dependencies using pip
|
||||||
|
cmds:
|
||||||
|
- pip install -r requirements.txt || pip install -e .
|
||||||
|
|
||||||
|
deps-update:
|
||||||
|
desc: Update dependencies
|
||||||
|
cmds:
|
||||||
|
- pip install --upgrade -r requirements.txt || pip install --upgrade -e .
|
||||||
|
|
||||||
|
version:
|
||||||
|
desc: Show current version
|
||||||
|
cmds:
|
||||||
|
- 'echo "Version: {{.VERSION}}"'
|
||||||
|
- 'echo "VCS Ref: {{.VCS_REF}}"'
|
||||||
|
- 'echo "Build Date: {{.BUILD_DATE}}"'
|
||||||
|
|
||||||
|
setup-dirs:
|
||||||
|
desc: Create required directories for running the node
|
||||||
|
cmds:
|
||||||
|
- mkdir -p pages files node-config
|
||||||
|
|
||||||
|
nix-shell:
|
||||||
|
desc: Enter Nix development shell
|
||||||
|
cmds:
|
||||||
|
- nix develop
|
||||||
|
|
||||||
|
nix-build:
|
||||||
|
desc: Build with Nix
|
||||||
|
cmds:
|
||||||
|
- nix build
|
||||||
|
|
||||||
|
all:
|
||||||
|
desc: Run build, lint, and test
|
||||||
|
deps: [build, check, test]
|
||||||
|
|
||||||
31
config.example
Normal file
31
config.example
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
# rns-page-node configuration file
|
||||||
|
# Lines starting with # are comments
|
||||||
|
# Format: key=value
|
||||||
|
|
||||||
|
# Reticulum config directory path
|
||||||
|
# reticulum-config=/path/to/reticulum/config
|
||||||
|
|
||||||
|
# Node display name
|
||||||
|
node-name=My Page Node
|
||||||
|
|
||||||
|
# Pages directory
|
||||||
|
pages-dir=./pages
|
||||||
|
|
||||||
|
# Files directory
|
||||||
|
files-dir=./files
|
||||||
|
|
||||||
|
# Node identity directory
|
||||||
|
identity-dir=./node-config
|
||||||
|
|
||||||
|
# Announce interval in minutes (default: 360 = 6 hours)
|
||||||
|
announce-interval=360
|
||||||
|
|
||||||
|
# Page refresh interval in seconds (0 = disabled)
|
||||||
|
page-refresh-interval=300
|
||||||
|
|
||||||
|
# File refresh interval in seconds (0 = disabled)
|
||||||
|
file-refresh-interval=300
|
||||||
|
|
||||||
|
# Log level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
|
||||||
|
log-level=INFO
|
||||||
|
|
||||||
36
docker/Dockerfile
Normal file
36
docker/Dockerfile
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
ARG PYTHON_VERSION=3.13
|
||||||
|
FROM python:${PYTHON_VERSION}-alpine
|
||||||
|
|
||||||
|
ARG BUILD_DATE
|
||||||
|
ARG VCS_REF
|
||||||
|
ARG VERSION
|
||||||
|
|
||||||
|
LABEL org.opencontainers.image.created=$BUILD_DATE \
|
||||||
|
org.opencontainers.image.title="RNS Page Node" \
|
||||||
|
org.opencontainers.image.description="A simple way to serve pages and files over the Reticulum network." \
|
||||||
|
org.opencontainers.image.url="https://git.quad4.io/RNS-Things/rns-page-node" \
|
||||||
|
org.opencontainers.image.documentation="https://git.quad4.io/RNS-Things/rns-page-node/src/branch/main/README.md" \
|
||||||
|
org.opencontainers.image.source="https://git.quad4.io/RNS-Things/rns-page-node" \
|
||||||
|
org.opencontainers.image.version=$VERSION \
|
||||||
|
org.opencontainers.image.revision=$VCS_REF \
|
||||||
|
org.opencontainers.image.vendor="RNS-Things" \
|
||||||
|
org.opencontainers.image.licenses="GPL-3.0" \
|
||||||
|
org.opencontainers.image.authors="Sudo-Ivan" \
|
||||||
|
org.opencontainers.image.base.name="python:${PYTHON_VERSION}-alpine"
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
RUN apk add --no-cache gcc python3-dev musl-dev linux-headers
|
||||||
|
|
||||||
|
RUN pip install poetry
|
||||||
|
ENV POETRY_VIRTUALENVS_IN_PROJECT=true
|
||||||
|
|
||||||
|
COPY pyproject.toml poetry.lock* ./
|
||||||
|
COPY README.md ./
|
||||||
|
COPY rns_page_node ./rns_page_node
|
||||||
|
|
||||||
|
RUN poetry install --no-interaction --no-ansi
|
||||||
|
|
||||||
|
ENV PATH="/app/.venv/bin:$PATH"
|
||||||
|
|
||||||
|
ENTRYPOINT ["poetry", "run", "rns-page-node"]
|
||||||
18
docker/Dockerfile.build
Normal file
18
docker/Dockerfile.build
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
FROM python:3.14-alpine AS builder
|
||||||
|
|
||||||
|
RUN apk update
|
||||||
|
RUN apk add --no-cache build-base libffi-dev cargo pkgconfig gcc python3-dev musl-dev linux-headers
|
||||||
|
|
||||||
|
WORKDIR /src
|
||||||
|
|
||||||
|
RUN pip install poetry
|
||||||
|
|
||||||
|
COPY pyproject.toml ./
|
||||||
|
COPY README.md ./
|
||||||
|
COPY rns_page_node ./rns_page_node
|
||||||
|
|
||||||
|
RUN poetry build --format wheel
|
||||||
|
|
||||||
|
FROM scratch AS dist
|
||||||
|
|
||||||
|
COPY --from=builder /src/dist .
|
||||||
40
docker/Dockerfile.rootless
Normal file
40
docker/Dockerfile.rootless
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
ARG PYTHON_VERSION=3.13
|
||||||
|
FROM python:${PYTHON_VERSION}-alpine
|
||||||
|
|
||||||
|
ARG BUILD_DATE
|
||||||
|
ARG VCS_REF
|
||||||
|
ARG VERSION
|
||||||
|
|
||||||
|
LABEL org.opencontainers.image.created=$BUILD_DATE \
|
||||||
|
org.opencontainers.image.title="RNS Page Node (Rootless)" \
|
||||||
|
org.opencontainers.image.description="A simple way to serve pages and files over the Reticulum network." \
|
||||||
|
org.opencontainers.image.url="https://git.quad4.io/RNS-Things/rns-page-node" \
|
||||||
|
org.opencontainers.image.documentation="https://git.quad4.io/RNS-Things/rns-page-node/src/branch/main/README.md" \
|
||||||
|
org.opencontainers.image.source="https://git.quad4.io/RNS-Things/rns-page-node" \
|
||||||
|
org.opencontainers.image.version=$VERSION \
|
||||||
|
org.opencontainers.image.revision=$VCS_REF \
|
||||||
|
org.opencontainers.image.vendor="RNS-Things" \
|
||||||
|
org.opencontainers.image.licenses="GPL-3.0" \
|
||||||
|
org.opencontainers.image.authors="Sudo-Ivan" \
|
||||||
|
org.opencontainers.image.base.name="python:${PYTHON_VERSION}-alpine"
|
||||||
|
|
||||||
|
RUN addgroup -g 1000 app && adduser -D -u 1000 -G app app
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
RUN apk add --no-cache gcc python3-dev musl-dev linux-headers
|
||||||
|
|
||||||
|
RUN pip install poetry
|
||||||
|
ENV POETRY_VIRTUALENVS_IN_PROJECT=true
|
||||||
|
|
||||||
|
COPY pyproject.toml poetry.lock* ./
|
||||||
|
COPY README.md ./
|
||||||
|
COPY rns_page_node ./rns_page_node
|
||||||
|
|
||||||
|
RUN poetry install --no-interaction --no-ansi
|
||||||
|
|
||||||
|
ENV PATH="/app/.venv/bin:$PATH"
|
||||||
|
|
||||||
|
USER app
|
||||||
|
|
||||||
|
ENTRYPOINT ["poetry", "run", "rns-page-node"]
|
||||||
14
docker/Dockerfile.tests
Normal file
14
docker/Dockerfile.tests
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
FROM python:3.14-slim
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y build-essential libssl-dev && rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY . /app
|
||||||
|
RUN pip install --no-cache-dir .
|
||||||
|
|
||||||
|
WORKDIR /app/tests
|
||||||
|
|
||||||
|
RUN chmod +x run_tests.sh
|
||||||
|
|
||||||
|
CMD ["bash", "run_tests.sh"]
|
||||||
61
flake.lock
generated
Normal file
61
flake.lock
generated
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
{
|
||||||
|
"nodes": {
|
||||||
|
"flake-utils": {
|
||||||
|
"inputs": {
|
||||||
|
"systems": "systems"
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1731533236,
|
||||||
|
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "numtide",
|
||||||
|
"repo": "flake-utils",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"nixpkgs": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1766902085,
|
||||||
|
"narHash": "sha256-coBu0ONtFzlwwVBzmjacUQwj3G+lybcZ1oeNSQkgC0M=",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "c0b0e0fddf73fd517c3471e546c0df87a42d53f4",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "NixOS",
|
||||||
|
"ref": "nixos-unstable",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"inputs": {
|
||||||
|
"flake-utils": "flake-utils",
|
||||||
|
"nixpkgs": "nixpkgs"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"systems": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1681028828,
|
||||||
|
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "nix-systems",
|
||||||
|
"repo": "default",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "root",
|
||||||
|
"version": 7
|
||||||
|
}
|
||||||
35
flake.nix
Normal file
35
flake.nix
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
{
|
||||||
|
description = "A simple way to serve pages and files over the Reticulum network";
|
||||||
|
|
||||||
|
inputs = {
|
||||||
|
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||||
|
flake-utils.url = "github:numtide/flake-utils";
|
||||||
|
};
|
||||||
|
|
||||||
|
outputs = { self, nixpkgs, flake-utils }:
|
||||||
|
flake-utils.lib.eachDefaultSystem (system:
|
||||||
|
let
|
||||||
|
pkgs = import nixpkgs {
|
||||||
|
inherit system;
|
||||||
|
};
|
||||||
|
|
||||||
|
python = pkgs.python3;
|
||||||
|
|
||||||
|
pythonPackages = python.pkgs;
|
||||||
|
in
|
||||||
|
{
|
||||||
|
devShells.default = pkgs.mkShell {
|
||||||
|
buildInputs = with pkgs; [
|
||||||
|
python
|
||||||
|
poetry
|
||||||
|
go-task
|
||||||
|
pythonPackages.build
|
||||||
|
pythonPackages.pip
|
||||||
|
pythonPackages.setuptools
|
||||||
|
pythonPackages.wheel
|
||||||
|
pythonPackages.ruff
|
||||||
|
];
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
329
poetry.lock
generated
329
poetry.lock
generated
@@ -1,147 +1,192 @@
|
|||||||
# 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]]
|
[[package]]
|
||||||
name = "cffi"
|
name = "cffi"
|
||||||
version = "1.17.1"
|
version = "2.0.0"
|
||||||
description = "Foreign Function Interface for Python calling C code."
|
description = "Foreign Function Interface for Python calling C code."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.9"
|
||||||
groups = ["main"]
|
groups = ["main"]
|
||||||
markers = "platform_python_implementation != \"PyPy\""
|
markers = "platform_python_implementation != \"PyPy\""
|
||||||
files = [
|
files = [
|
||||||
{file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"},
|
{file = "cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44"},
|
||||||
{file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"},
|
{file = "cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49"},
|
||||||
{file = "cffi-1.17.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:edae79245293e15384b51f88b00613ba9f7198016a5948b5dddf4917d4d26382"},
|
{file = "cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c"},
|
||||||
{file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45398b671ac6d70e67da8e4224a065cec6a93541bb7aebe1b198a61b58c7b702"},
|
{file = "cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb"},
|
||||||
{file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad9413ccdeda48c5afdae7e4fa2192157e991ff761e7ab8fdd8926f40b160cc3"},
|
{file = "cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0"},
|
||||||
{file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5da5719280082ac6bd9aa7becb3938dc9f9cbd57fac7d2871717b1feb0902ab6"},
|
{file = "cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4"},
|
||||||
{file = "cffi-1.17.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bb1a08b8008b281856e5971307cc386a8e9c5b625ac297e853d36da6efe9c17"},
|
{file = "cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453"},
|
||||||
{file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:045d61c734659cc045141be4bae381a41d89b741f795af1dd018bfb532fd0df8"},
|
{file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495"},
|
||||||
{file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6883e737d7d9e4899a8a695e00ec36bd4e5e4f18fabe0aca0efe0a4b44cdb13e"},
|
{file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5"},
|
||||||
{file = "cffi-1.17.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6b8b4a92e1c65048ff98cfe1f735ef8f1ceb72e3d5f0c25fdb12087a23da22be"},
|
{file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb"},
|
||||||
{file = "cffi-1.17.1-cp310-cp310-win32.whl", hash = "sha256:c9c3d058ebabb74db66e431095118094d06abf53284d9c81f27300d0e0d8bc7c"},
|
{file = "cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a"},
|
||||||
{file = "cffi-1.17.1-cp310-cp310-win_amd64.whl", hash = "sha256:0f048dcf80db46f0098ccac01132761580d28e28bc0f78ae0d58048063317e15"},
|
{file = "cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739"},
|
||||||
{file = "cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401"},
|
{file = "cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe"},
|
||||||
{file = "cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf"},
|
{file = "cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c"},
|
||||||
{file = "cffi-1.17.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f75c7ab1f9e4aca5414ed4d8e5c0e303a34f4421f8a0d47a4d019ceff0ab6af4"},
|
{file = "cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92"},
|
||||||
{file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a1ed2dd2972641495a3ec98445e09766f077aee98a1c896dcb4ad0d303628e41"},
|
{file = "cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93"},
|
||||||
{file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:46bf43160c1a35f7ec506d254e5c890f3c03648a4dbac12d624e4490a7046cd1"},
|
{file = "cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5"},
|
||||||
{file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a24ed04c8ffd54b0729c07cee15a81d964e6fee0e3d4d342a27b020d22959dc6"},
|
{file = "cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664"},
|
||||||
{file = "cffi-1.17.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:610faea79c43e44c71e1ec53a554553fa22321b65fae24889706c0a84d4ad86d"},
|
{file = "cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26"},
|
||||||
{file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a9b15d491f3ad5d692e11f6b71f7857e7835eb677955c00cc0aefcd0669adaf6"},
|
{file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9"},
|
||||||
{file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:de2ea4b5833625383e464549fec1bc395c1bdeeb5f25c4a3a82b5a8c756ec22f"},
|
{file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414"},
|
||||||
{file = "cffi-1.17.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fc48c783f9c87e60831201f2cce7f3b2e4846bf4d8728eabe54d60700b318a0b"},
|
{file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743"},
|
||||||
{file = "cffi-1.17.1-cp311-cp311-win32.whl", hash = "sha256:85a950a4ac9c359340d5963966e3e0a94a676bd6245a4b55bc43949eee26a655"},
|
{file = "cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5"},
|
||||||
{file = "cffi-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:caaf0640ef5f5517f49bc275eca1406b0ffa6aa184892812030f04c2abf589a0"},
|
{file = "cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5"},
|
||||||
{file = "cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4"},
|
{file = "cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d"},
|
||||||
{file = "cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c"},
|
{file = "cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d"},
|
||||||
{file = "cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36"},
|
{file = "cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c"},
|
||||||
{file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5"},
|
{file = "cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe"},
|
||||||
{file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff"},
|
{file = "cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062"},
|
||||||
{file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99"},
|
{file = "cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e"},
|
||||||
{file = "cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93"},
|
{file = "cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037"},
|
||||||
{file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3"},
|
{file = "cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba"},
|
||||||
{file = "cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8"},
|
{file = "cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94"},
|
||||||
{file = "cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65"},
|
{file = "cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187"},
|
||||||
{file = "cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903"},
|
{file = "cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18"},
|
||||||
{file = "cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e"},
|
{file = "cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5"},
|
||||||
{file = "cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2"},
|
{file = "cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6"},
|
||||||
{file = "cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3"},
|
{file = "cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb"},
|
||||||
{file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683"},
|
{file = "cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca"},
|
||||||
{file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5"},
|
{file = "cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b"},
|
||||||
{file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4"},
|
{file = "cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b"},
|
||||||
{file = "cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd"},
|
{file = "cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2"},
|
||||||
{file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed"},
|
{file = "cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3"},
|
||||||
{file = "cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9"},
|
{file = "cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26"},
|
||||||
{file = "cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d"},
|
{file = "cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c"},
|
||||||
{file = "cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a"},
|
{file = "cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b"},
|
||||||
{file = "cffi-1.17.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:636062ea65bd0195bc012fea9321aca499c0504409f413dc88af450b57ffd03b"},
|
{file = "cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27"},
|
||||||
{file = "cffi-1.17.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c7eac2ef9b63c79431bc4b25f1cd649d7f061a28808cbc6c47b534bd789ef964"},
|
{file = "cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75"},
|
||||||
{file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e221cf152cff04059d011ee126477f0d9588303eb57e88923578ace7baad17f9"},
|
{file = "cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91"},
|
||||||
{file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:31000ec67d4221a71bd3f67df918b1f88f676f1c3b535a7eb473255fdc0b83fc"},
|
{file = "cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5"},
|
||||||
{file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f17be4345073b0a7b8ea599688f692ac3ef23ce28e5df79c04de519dbc4912c"},
|
{file = "cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13"},
|
||||||
{file = "cffi-1.17.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0e2b1fac190ae3ebfe37b979cc1ce69c81f4e4fe5746bb401dca63a9062cdaf1"},
|
{file = "cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b"},
|
||||||
{file = "cffi-1.17.1-cp38-cp38-win32.whl", hash = "sha256:7596d6620d3fa590f677e9ee430df2958d2d6d6de2feeae5b20e82c00b76fbf8"},
|
{file = "cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c"},
|
||||||
{file = "cffi-1.17.1-cp38-cp38-win_amd64.whl", hash = "sha256:78122be759c3f8a014ce010908ae03364d00a1f81ab5c7f4a7a5120607ea56e1"},
|
{file = "cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef"},
|
||||||
{file = "cffi-1.17.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b2ab587605f4ba0bf81dc0cb08a41bd1c0a5906bd59243d56bad7668a6fc6c16"},
|
{file = "cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775"},
|
||||||
{file = "cffi-1.17.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:28b16024becceed8c6dfbc75629e27788d8a3f9030691a1dbf9821a128b22c36"},
|
{file = "cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205"},
|
||||||
{file = "cffi-1.17.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d599671f396c4723d016dbddb72fe8e0397082b0a77a4fab8028923bec050e8"},
|
{file = "cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1"},
|
||||||
{file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca74b8dbe6e8e8263c0ffd60277de77dcee6c837a3d0881d8c1ead7268c9e576"},
|
{file = "cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f"},
|
||||||
{file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f7f5baafcc48261359e14bcd6d9bff6d4b28d9103847c9e136694cb0501aef87"},
|
{file = "cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25"},
|
||||||
{file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98e3969bcff97cae1b2def8ba499ea3d6f31ddfdb7635374834cf89a1a08ecf0"},
|
{file = "cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad"},
|
||||||
{file = "cffi-1.17.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdf5ce3acdfd1661132f2a9c19cac174758dc2352bfe37d98aa7512c6b7178b3"},
|
{file = "cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9"},
|
||||||
{file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9755e4345d1ec879e3849e62222a18c7174d65a6a92d5b346b1863912168b595"},
|
{file = "cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d"},
|
||||||
{file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f1e22e8c4419538cb197e4dd60acc919d7696e5ef98ee4da4e01d3f8cfa4cc5a"},
|
{file = "cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c"},
|
||||||
{file = "cffi-1.17.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c03e868a0b3bc35839ba98e74211ed2b05d2119be4e8a0f224fba9384f1fe02e"},
|
{file = "cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8"},
|
||||||
{file = "cffi-1.17.1-cp39-cp39-win32.whl", hash = "sha256:e31ae45bc2e29f6b2abd0de1cc3b9d5205aa847cafaecb8af1476a609a2f6eb7"},
|
{file = "cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc"},
|
||||||
{file = "cffi-1.17.1-cp39-cp39-win_amd64.whl", hash = "sha256:d016c76bdd850f3c626af19b0542c9677ba156e4ee4fccfdd7848803533ef662"},
|
{file = "cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592"},
|
||||||
{file = "cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824"},
|
{file = "cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512"},
|
||||||
|
{file = "cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4"},
|
||||||
|
{file = "cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e"},
|
||||||
|
{file = "cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6"},
|
||||||
|
{file = "cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9"},
|
||||||
|
{file = "cffi-2.0.0-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf"},
|
||||||
|
{file = "cffi-2.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:de8dad4425a6ca6e4e5e297b27b5c824ecc7581910bf9aee86cb6835e6812aa7"},
|
||||||
|
{file = "cffi-2.0.0-cp39-cp39-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:4647afc2f90d1ddd33441e5b0e85b16b12ddec4fca55f0d9671fef036ecca27c"},
|
||||||
|
{file = "cffi-2.0.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3f4d46d8b35698056ec29bca21546e1551a205058ae1a181d871e278b0b28165"},
|
||||||
|
{file = "cffi-2.0.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e6e73b9e02893c764e7e8d5bb5ce277f1a009cd5243f8228f75f842bf937c534"},
|
||||||
|
{file = "cffi-2.0.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:cb527a79772e5ef98fb1d700678fe031e353e765d1ca2d409c92263c6d43e09f"},
|
||||||
|
{file = "cffi-2.0.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:61d028e90346df14fedc3d1e5441df818d095f3b87d286825dfcbd6459b7ef63"},
|
||||||
|
{file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0f6084a0ea23d05d20c3edcda20c3d006f9b6f3fefeac38f59262e10cef47ee2"},
|
||||||
|
{file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1cd13c99ce269b3ed80b417dcd591415d3372bcac067009b6e0f59c7d4015e65"},
|
||||||
|
{file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:89472c9762729b5ae1ad974b777416bfda4ac5642423fa93bd57a09204712322"},
|
||||||
|
{file = "cffi-2.0.0-cp39-cp39-win32.whl", hash = "sha256:2081580ebb843f759b9f617314a24ed5738c51d2aee65d31e02f6f7a2b97707a"},
|
||||||
|
{file = "cffi-2.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:b882b3df248017dba09d6b16defe9b5c407fe32fc7c65a9c69798e6175601be9"},
|
||||||
|
{file = "cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
pycparser = "*"
|
pycparser = {version = "*", markers = "implementation_name != \"PyPy\""}
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cryptography"
|
name = "cryptography"
|
||||||
version = "43.0.3"
|
version = "46.0.3"
|
||||||
description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
|
description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
python-versions = "!=3.9.0,!=3.9.1,>=3.8"
|
||||||
groups = ["main"]
|
groups = ["main"]
|
||||||
files = [
|
files = [
|
||||||
{file = "cryptography-43.0.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:bf7a1932ac4176486eab36a19ed4c0492da5d97123f1406cf15e41b05e787d2e"},
|
{file = "cryptography-46.0.3-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:109d4ddfadf17e8e7779c39f9b18111a09efb969a301a31e987416a0191ed93a"},
|
||||||
{file = "cryptography-43.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63efa177ff54aec6e1c0aefaa1a241232dcd37413835a9b674b6e3f0ae2bfd3e"},
|
{file = "cryptography-46.0.3-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:09859af8466b69bc3c27bdf4f5d84a665e0f7ab5088412e9e2ec49758eca5cbc"},
|
||||||
{file = "cryptography-43.0.3-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e1ce50266f4f70bf41a2c6dc4358afadae90e2a1e5342d3c08883df1675374f"},
|
{file = "cryptography-46.0.3-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:01ca9ff2885f3acc98c29f1860552e37f6d7c7d013d7334ff2a9de43a449315d"},
|
||||||
{file = "cryptography-43.0.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:443c4a81bb10daed9a8f334365fe52542771f25aedaf889fd323a853ce7377d6"},
|
{file = "cryptography-46.0.3-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6eae65d4c3d33da080cff9c4ab1f711b15c1d9760809dad6ea763f3812d254cb"},
|
||||||
{file = "cryptography-43.0.3-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:74f57f24754fe349223792466a709f8e0c093205ff0dca557af51072ff47ab18"},
|
{file = "cryptography-46.0.3-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5bf0ed4490068a2e72ac03d786693adeb909981cc596425d09032d372bcc849"},
|
||||||
{file = "cryptography-43.0.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9762ea51a8fc2a88b70cf2995e5675b38d93bf36bd67d91721c309df184f49bd"},
|
{file = "cryptography-46.0.3-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:5ecfccd2329e37e9b7112a888e76d9feca2347f12f37918facbb893d7bb88ee8"},
|
||||||
{file = "cryptography-43.0.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:81ef806b1fef6b06dcebad789f988d3b37ccaee225695cf3e07648eee0fc6b73"},
|
{file = "cryptography-46.0.3-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:a2c0cd47381a3229c403062f764160d57d4d175e022c1df84e168c6251a22eec"},
|
||||||
{file = "cryptography-43.0.3-cp37-abi3-win32.whl", hash = "sha256:cbeb489927bd7af4aa98d4b261af9a5bc025bd87f0e3547e11584be9e9427be2"},
|
{file = "cryptography-46.0.3-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:549e234ff32571b1f4076ac269fcce7a808d3bf98b76c8dd560e42dbc66d7d91"},
|
||||||
{file = "cryptography-43.0.3-cp37-abi3-win_amd64.whl", hash = "sha256:f46304d6f0c6ab8e52770addfa2fc41e6629495548862279641972b6215451cd"},
|
{file = "cryptography-46.0.3-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:c0a7bb1a68a5d3471880e264621346c48665b3bf1c3759d682fc0864c540bd9e"},
|
||||||
{file = "cryptography-43.0.3-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:8ac43ae87929a5982f5948ceda07001ee5e83227fd69cf55b109144938d96984"},
|
{file = "cryptography-46.0.3-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:10b01676fc208c3e6feeb25a8b83d81767e8059e1fe86e1dc62d10a3018fa926"},
|
||||||
{file = "cryptography-43.0.3-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:846da004a5804145a5f441b8530b4bf35afbf7da70f82409f151695b127213d5"},
|
{file = "cryptography-46.0.3-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0abf1ffd6e57c67e92af68330d05760b7b7efb243aab8377e583284dbab72c71"},
|
||||||
{file = "cryptography-43.0.3-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f996e7268af62598f2fc1204afa98a3b5712313a55c4c9d434aef49cadc91d4"},
|
{file = "cryptography-46.0.3-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a04bee9ab6a4da801eb9b51f1b708a1b5b5c9eb48c03f74198464c66f0d344ac"},
|
||||||
{file = "cryptography-43.0.3-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:f7b178f11ed3664fd0e995a47ed2b5ff0a12d893e41dd0494f406d1cf555cab7"},
|
{file = "cryptography-46.0.3-cp311-abi3-win32.whl", hash = "sha256:f260d0d41e9b4da1ed1e0f1ce571f97fe370b152ab18778e9e8f67d6af432018"},
|
||||||
{file = "cryptography-43.0.3-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:c2e6fc39c4ab499049df3bdf567f768a723a5e8464816e8f009f121a5a9f4405"},
|
{file = "cryptography-46.0.3-cp311-abi3-win_amd64.whl", hash = "sha256:a9a3008438615669153eb86b26b61e09993921ebdd75385ddd748702c5adfddb"},
|
||||||
{file = "cryptography-43.0.3-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e1be4655c7ef6e1bbe6b5d0403526601323420bcf414598955968c9ef3eb7d16"},
|
{file = "cryptography-46.0.3-cp311-abi3-win_arm64.whl", hash = "sha256:5d7f93296ee28f68447397bf5198428c9aeeab45705a55d53a6343455dcb2c3c"},
|
||||||
{file = "cryptography-43.0.3-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:df6b6c6d742395dd77a23ea3728ab62f98379eff8fb61be2744d4679ab678f73"},
|
{file = "cryptography-46.0.3-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:00a5e7e87938e5ff9ff5447ab086a5706a957137e6e433841e9d24f38a065217"},
|
||||||
{file = "cryptography-43.0.3-cp39-abi3-win32.whl", hash = "sha256:d56e96520b1020449bbace2b78b603442e7e378a9b3bd68de65c782db1507995"},
|
{file = "cryptography-46.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c8daeb2d2174beb4575b77482320303f3d39b8e81153da4f0fb08eb5fe86a6c5"},
|
||||||
{file = "cryptography-43.0.3-cp39-abi3-win_amd64.whl", hash = "sha256:0c580952eef9bf68c4747774cde7ec1d85a6e61de97281f2dba83c7d2c806362"},
|
{file = "cryptography-46.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:39b6755623145ad5eff1dab323f4eae2a32a77a7abef2c5089a04a3d04366715"},
|
||||||
{file = "cryptography-43.0.3-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d03b5621a135bffecad2c73e9f4deb1a0f977b9a8ffe6f8e002bf6c9d07b918c"},
|
{file = "cryptography-46.0.3-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:db391fa7c66df6762ee3f00c95a89e6d428f4d60e7abc8328f4fe155b5ac6e54"},
|
||||||
{file = "cryptography-43.0.3-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:a2a431ee15799d6db9fe80c82b055bae5a752bef645bba795e8e52687c69efe3"},
|
{file = "cryptography-46.0.3-cp314-cp314t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:78a97cf6a8839a48c49271cdcbd5cf37ca2c1d6b7fdd86cc864f302b5e9bf459"},
|
||||||
{file = "cryptography-43.0.3-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:281c945d0e28c92ca5e5930664c1cefd85efe80e5c0d2bc58dd63383fda29f83"},
|
{file = "cryptography-46.0.3-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:dfb781ff7eaa91a6f7fd41776ec37c5853c795d3b358d4896fdbb5df168af422"},
|
||||||
{file = "cryptography-43.0.3-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:f18c716be16bc1fea8e95def49edf46b82fccaa88587a45f8dc0ff6ab5d8e0a7"},
|
{file = "cryptography-46.0.3-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:6f61efb26e76c45c4a227835ddeae96d83624fb0d29eb5df5b96e14ed1a0afb7"},
|
||||||
{file = "cryptography-43.0.3-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:4a02ded6cd4f0a5562a8887df8b3bd14e822a90f97ac5e544c162899bc467664"},
|
{file = "cryptography-46.0.3-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:23b1a8f26e43f47ceb6d6a43115f33a5a37d57df4ea0ca295b780ae8546e8044"},
|
||||||
{file = "cryptography-43.0.3-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:53a583b6637ab4c4e3591a15bc9db855b8d9dee9a669b550f311480acab6eb08"},
|
{file = "cryptography-46.0.3-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:b419ae593c86b87014b9be7396b385491ad7f320bde96826d0dd174459e54665"},
|
||||||
{file = "cryptography-43.0.3-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:1ec0bcf7e17c0c5669d881b1cd38c4972fade441b27bda1051665faaa89bdcaa"},
|
{file = "cryptography-46.0.3-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:50fc3343ac490c6b08c0cf0d704e881d0d660be923fd3076db3e932007e726e3"},
|
||||||
{file = "cryptography-43.0.3-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2ce6fae5bdad59577b44e4dfed356944fbf1d925269114c28be377692643b4ff"},
|
{file = "cryptography-46.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:22d7e97932f511d6b0b04f2bfd818d73dcd5928db509460aaf48384778eb6d20"},
|
||||||
{file = "cryptography-43.0.3.tar.gz", hash = "sha256:315b9001266a492a6ff443b61238f956b214dbec9910a081ba5b6646a055a805"},
|
{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]
|
[package.dependencies]
|
||||||
cffi = {version = ">=1.12", markers = "platform_python_implementation != \"PyPy\""}
|
cffi = {version = ">=2.0.0", markers = "python_full_version >= \"3.9.0\" and platform_python_implementation != \"PyPy\""}
|
||||||
|
typing-extensions = {version = ">=4.13.2", markers = "python_full_version < \"3.11.0\""}
|
||||||
|
|
||||||
[package.extras]
|
[package.extras]
|
||||||
docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"]
|
docs = ["sphinx (>=5.3.0)", "sphinx-inline-tabs", "sphinx-rtd-theme (>=3.0.0)"]
|
||||||
docstest = ["pyenchant (>=1.6.11)", "readme-renderer", "sphinxcontrib-spelling (>=4.0.1)"]
|
docstest = ["pyenchant (>=3)", "readme-renderer (>=30.0)", "sphinxcontrib-spelling (>=7.3.1)"]
|
||||||
nox = ["nox"]
|
nox = ["nox[uv] (>=2024.4.15)"]
|
||||||
pep8test = ["check-sdist", "click", "mypy", "ruff"]
|
pep8test = ["check-sdist", "click (>=8.0.1)", "mypy (>=1.14)", "ruff (>=0.11.11)"]
|
||||||
sdist = ["build"]
|
sdist = ["build (>=1.0.0)"]
|
||||||
ssh = ["bcrypt (>=3.1.5)"]
|
ssh = ["bcrypt (>=3.1.5)"]
|
||||||
test = ["certifi", "cryptography-vectors (==43.0.3)", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"]
|
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"]
|
test-randomorder = ["pytest-randomly"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pycparser"
|
name = "pycparser"
|
||||||
version = "2.22"
|
version = "2.23"
|
||||||
description = "C parser in Python"
|
description = "C parser in Python"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
groups = ["main"]
|
groups = ["main"]
|
||||||
markers = "platform_python_implementation != \"PyPy\""
|
markers = "platform_python_implementation != \"PyPy\" and implementation_name != \"PyPy\""
|
||||||
files = [
|
files = [
|
||||||
{file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"},
|
{file = "pycparser-2.23-py3-none-any.whl", hash = "sha256:e5c6e8d3fbad53479cab09ac03729e0a9faf2bee3db8208a550daf5af81a5934"},
|
||||||
{file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"},
|
{file = "pycparser-2.23.tar.gz", hash = "sha256:78816d4f24add8f10a06d6f05b4d424ad9e96cfebf68a4ddc99c65c0720d00c2"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -161,13 +206,15 @@ cp2110 = ["hidapi"]
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rns"
|
name = "rns"
|
||||||
version = "0.9.6"
|
version = "1.0.4"
|
||||||
description = "Self-configuring, encrypted and resilient mesh networking stack for LoRa, packet radio, WiFi and everything in between"
|
description = "Self-configuring, encrypted and resilient mesh networking stack for LoRa, packet radio, WiFi and everything in between"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
python-versions = ">=3.7"
|
||||||
groups = ["main"]
|
groups = ["main"]
|
||||||
files = [
|
files = [
|
||||||
{file = "rns-0.9.6-py3-none-any.whl", hash = "sha256:a23c64a04c1e83fd0ab449f564ac904da7fd4f61c0faf68a063f486cc48b44bd"},
|
{file = "rns-1.0.4-1-py3-none-any.whl", hash = "sha256:f1804f8b07a8b8e1c1b61889f929fdb5cfbd57f4c354108c417135f0d67c5ef6"},
|
||||||
|
{file = "rns-1.0.4-py3-none-any.whl", hash = "sha256:7a2b7893410833b42c0fa7f9a9e3369cebb085cdd26bd83f3031fa6c1051653c"},
|
||||||
|
{file = "rns-1.0.4.tar.gz", hash = "sha256:e70667a767fe523bab8e7ea0627447258c4e6763b7756fbba50c6556dbb84399"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
@@ -176,33 +223,47 @@ pyserial = ">=3.5"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ruff"
|
name = "ruff"
|
||||||
version = "0.11.11"
|
version = "0.14.10"
|
||||||
description = "An extremely fast Python linter and code formatter, written in Rust."
|
description = "An extremely fast Python linter and code formatter, written in Rust."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
python-versions = ">=3.7"
|
||||||
groups = ["dev"]
|
groups = ["dev"]
|
||||||
files = [
|
files = [
|
||||||
{file = "ruff-0.11.11-py3-none-linux_armv6l.whl", hash = "sha256:9924e5ae54125ed8958a4f7de320dab7380f6e9fa3195e3dc3b137c6842a0092"},
|
{file = "ruff-0.14.10-py3-none-linux_armv6l.whl", hash = "sha256:7a3ce585f2ade3e1f29ec1b92df13e3da262178df8c8bdf876f48fa0e8316c49"},
|
||||||
{file = "ruff-0.11.11-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:c8a93276393d91e952f790148eb226658dd275cddfde96c6ca304873f11d2ae4"},
|
{file = "ruff-0.14.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:674f9be9372907f7257c51f1d4fc902cb7cf014b9980152b802794317941f08f"},
|
||||||
{file = "ruff-0.11.11-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d6e333dbe2e6ae84cdedefa943dfd6434753ad321764fd937eef9d6b62022bcd"},
|
{file = "ruff-0.14.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d85713d522348837ef9df8efca33ccb8bd6fcfc86a2cde3ccb4bc9d28a18003d"},
|
||||||
{file = "ruff-0.11.11-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7885d9a5e4c77b24e8c88aba8c80be9255fa22ab326019dac2356cff42089fc6"},
|
{file = "ruff-0.14.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6987ebe0501ae4f4308d7d24e2d0fe3d7a98430f5adfd0f1fead050a740a3a77"},
|
||||||
{file = "ruff-0.11.11-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1b5ab797fcc09121ed82e9b12b6f27e34859e4227080a42d090881be888755d4"},
|
{file = "ruff-0.14.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:16a01dfb7b9e4eee556fbfd5392806b1b8550c9b4a9f6acd3dbe6812b193c70a"},
|
||||||
{file = "ruff-0.11.11-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e231ff3132c1119ece836487a02785f099a43992b95c2f62847d29bace3c75ac"},
|
{file = "ruff-0.14.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7165d31a925b7a294465fa81be8c12a0e9b60fb02bf177e79067c867e71f8b1f"},
|
||||||
{file = "ruff-0.11.11-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:a97c9babe1d4081037a90289986925726b802d180cca784ac8da2bbbc335f709"},
|
{file = "ruff-0.14.10-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:c561695675b972effb0c0a45db233f2c816ff3da8dcfbe7dfc7eed625f218935"},
|
||||||
{file = "ruff-0.11.11-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d8c4ddcbe8a19f59f57fd814b8b117d4fcea9bee7c0492e6cf5fdc22cfa563c8"},
|
{file = "ruff-0.14.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4bb98fcbbc61725968893682fd4df8966a34611239c9fd07a1f6a07e7103d08e"},
|
||||||
{file = "ruff-0.11.11-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6224076c344a7694c6fbbb70d4f2a7b730f6d47d2a9dc1e7f9d9bb583faf390b"},
|
{file = "ruff-0.14.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f24b47993a9d8cb858429e97bdf8544c78029f09b520af615c1d261bf827001d"},
|
||||||
{file = "ruff-0.11.11-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:882821fcdf7ae8db7a951df1903d9cb032bbe838852e5fc3c2b6c3ab54e39875"},
|
{file = "ruff-0.14.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59aabd2e2c4fd614d2862e7939c34a532c04f1084476d6833dddef4afab87e9f"},
|
||||||
{file = "ruff-0.11.11-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:dcec2d50756463d9df075a26a85a6affbc1b0148873da3997286caf1ce03cae1"},
|
{file = "ruff-0.14.10-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:213db2b2e44be8625002dbea33bb9c60c66ea2c07c084a00d55732689d697a7f"},
|
||||||
{file = "ruff-0.11.11-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:99c28505ecbaeb6594701a74e395b187ee083ee26478c1a795d35084d53ebd81"},
|
{file = "ruff-0.14.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:b914c40ab64865a17a9a5b67911d14df72346a634527240039eb3bd650e5979d"},
|
||||||
{file = "ruff-0.11.11-py3-none-musllinux_1_2_i686.whl", hash = "sha256:9263f9e5aa4ff1dec765e99810f1cc53f0c868c5329b69f13845f699fe74f639"},
|
{file = "ruff-0.14.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:1484983559f026788e3a5c07c81ef7d1e97c1c78ed03041a18f75df104c45405"},
|
||||||
{file = "ruff-0.11.11-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:64ac6f885e3ecb2fdbb71de2701d4e34526651f1e8503af8fb30d4915a3fe345"},
|
{file = "ruff-0.14.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c70427132db492d25f982fffc8d6c7535cc2fd2c83fc8888f05caaa248521e60"},
|
||||||
{file = "ruff-0.11.11-py3-none-win32.whl", hash = "sha256:1adcb9a18802268aaa891ffb67b1c94cd70578f126637118e8099b8e4adcf112"},
|
{file = "ruff-0.14.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5bcf45b681e9f1ee6445d317ce1fa9d6cba9a6049542d1c3d5b5958986be8830"},
|
||||||
{file = "ruff-0.11.11-py3-none-win_amd64.whl", hash = "sha256:748b4bb245f11e91a04a4ff0f96e386711df0a30412b9fe0c74d5bdc0e4a531f"},
|
{file = "ruff-0.14.10-py3-none-win32.whl", hash = "sha256:104c49fc7ab73f3f3a758039adea978869a918f31b73280db175b43a2d9b51d6"},
|
||||||
{file = "ruff-0.11.11-py3-none-win_arm64.whl", hash = "sha256:6c51f136c0364ab1b774767aa8b86331bd8e9d414e2d107db7a2189f35ea1f7b"},
|
{file = "ruff-0.14.10-py3-none-win_amd64.whl", hash = "sha256:466297bd73638c6bdf06485683e812db1c00c7ac96d4ddd0294a338c62fdc154"},
|
||||||
{file = "ruff-0.11.11.tar.gz", hash = "sha256:7774173cc7c1980e6bf67569ebb7085989a78a103922fb83ef3dfe230cd0687d"},
|
{file = "ruff-0.14.10-py3-none-win_arm64.whl", hash = "sha256:e51d046cf6dda98a4633b8a8a771451107413b0f07183b2bef03f075599e44e6"},
|
||||||
|
{file = "ruff-0.14.10.tar.gz", hash = "sha256:9a2e830f075d1a42cd28420d7809ace390832a490ed0966fe373ba288e77aaf4"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "typing-extensions"
|
||||||
|
version = "4.15.0"
|
||||||
|
description = "Backported and Experimental Type Hints for Python 3.9+"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.9"
|
||||||
|
groups = ["main"]
|
||||||
|
markers = "python_full_version < \"3.11.0\""
|
||||||
|
files = [
|
||||||
|
{file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"},
|
||||||
|
{file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "2.1"
|
lock-version = "2.1"
|
||||||
python-versions = ">=3.9"
|
python-versions = ">=3.9.2"
|
||||||
content-hash = "950367dd4139ddc3edccc7efbdcd9236c740eb4f393f6973b8af49c76a657cd0"
|
content-hash = "42d1d286b79ed42d6a0fe6adf1cb3e7c730967cd82b9013c580851a65b5fcbdc"
|
||||||
|
|||||||
@@ -1,16 +1,25 @@
|
|||||||
[project]
|
[project]
|
||||||
name = "rns-page-node"
|
name = "rns-page-node"
|
||||||
version = "0.1.0"
|
version = "1.3.0"
|
||||||
license = {file = "LICENSE"}
|
license = "GPL-3.0-only"
|
||||||
description = "A simple way to serve pages and files over the Reticulum network."
|
description = "A simple way to serve pages and files over the Reticulum network."
|
||||||
authors = [
|
authors = [
|
||||||
{name = "Sudo-Ivan"}
|
{name = "Sudo-Ivan"}
|
||||||
]
|
]
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.9"
|
requires-python = ">=3.9.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"rns (>=0.9.6,<0.10.0)"
|
"rns (>=1.0.4,<1.5.0)",
|
||||||
|
"cryptography>=46.0.3"
|
||||||
]
|
]
|
||||||
|
classifiers = [
|
||||||
|
"Programming Language :: Python :: 3",
|
||||||
|
"Operating System :: OS Independent",
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.urls]
|
||||||
|
Homepage = "https://git.quad4.io/RNS-Things/rns-page-node"
|
||||||
|
Repository = "https://git.quad4.io/RNS-Things/rns-page-node"
|
||||||
|
|
||||||
[project.scripts]
|
[project.scripts]
|
||||||
rns-page-node = "rns_page_node.main:main"
|
rns-page-node = "rns_page_node.main:main"
|
||||||
@@ -20,5 +29,4 @@ requires = ["poetry-core>=2.0.0,<3.0.0"]
|
|||||||
build-backend = "poetry.core.masonry.api"
|
build-backend = "poetry.core.masonry.api"
|
||||||
|
|
||||||
[tool.poetry.group.dev.dependencies]
|
[tool.poetry.group.dev.dependencies]
|
||||||
ruff = "^0.11.11"
|
ruff = "^0.14.10"
|
||||||
|
|
||||||
|
|||||||
3
renovate.json
Normal file
3
renovate.json
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"$schema": "https://docs.renovatebot.com/renovate-schema.json"
|
||||||
|
}
|
||||||
@@ -1 +1,2 @@
|
|||||||
rns==0.9.6
|
rns=1.0.4
|
||||||
|
cryptography==46.0.3
|
||||||
@@ -1,2 +1,6 @@
|
|||||||
# rns_page_node package
|
"""RNS Page Node package.
|
||||||
__all__ = ['main']
|
|
||||||
|
A minimal Reticulum page node that serves .mu pages and files over RNS.
|
||||||
|
"""
|
||||||
|
|
||||||
|
__all__ = ["main"]
|
||||||
|
|||||||
@@ -1,28 +1,96 @@
|
|||||||
#!/usr/bin/env python3
|
"""Minimal Reticulum Page Node.
|
||||||
"""
|
|
||||||
Minimal Reticulum Page Node
|
|
||||||
Serves .mu pages and files over RNS.
|
Serves .mu pages and files over RNS.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
|
||||||
import time
|
|
||||||
import threading
|
|
||||||
import subprocess
|
|
||||||
import RNS
|
|
||||||
import argparse
|
import argparse
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
DEFAULT_INDEX = '''>Default Home Page
|
import RNS
|
||||||
|
|
||||||
This node is serving pages using page node, but the home page file (index.mu) was not found in the pages directory. Please add an index.mu file to customize the home page.
|
DEFAULT_INDEX = """>Default Home Page
|
||||||
'''
|
|
||||||
|
|
||||||
DEFAULT_NOTALLOWED = '''>Request Not Allowed
|
This node is serving pages using rns-page-node, but index.mu was not found.
|
||||||
|
Please add an index.mu file to customize the home page.
|
||||||
|
"""
|
||||||
|
|
||||||
|
DEFAULT_NOTALLOWED = """>Request Not Allowed
|
||||||
|
|
||||||
You are not authorised to carry out the request.
|
You are not authorised to carry out the request.
|
||||||
'''
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def load_config(config_file):
|
||||||
|
"""Load configuration from a plain text config file.
|
||||||
|
|
||||||
|
Config format is simple key=value pairs, one per line.
|
||||||
|
Lines starting with # are comments and are ignored.
|
||||||
|
Empty lines are ignored.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
config_file: Path to the config file
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary of configuration values
|
||||||
|
|
||||||
|
"""
|
||||||
|
config = {}
|
||||||
|
try:
|
||||||
|
with open(config_file, encoding="utf-8") as f:
|
||||||
|
for line_num, line in enumerate(f, 1):
|
||||||
|
line = line.strip()
|
||||||
|
if not line or line.startswith("#"):
|
||||||
|
continue
|
||||||
|
if "=" not in line:
|
||||||
|
RNS.log(
|
||||||
|
f"Invalid config line {line_num} in {config_file}: {line}",
|
||||||
|
RNS.LOG_WARNING,
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
key, value = line.split("=", 1)
|
||||||
|
key = key.strip()
|
||||||
|
value = value.strip()
|
||||||
|
if key and value:
|
||||||
|
config[key] = value
|
||||||
|
RNS.log(f"Loaded configuration from {config_file}", RNS.LOG_INFO)
|
||||||
|
except FileNotFoundError:
|
||||||
|
RNS.log(f"Config file not found: {config_file}", RNS.LOG_ERROR)
|
||||||
|
except Exception as e:
|
||||||
|
RNS.log(f"Error reading config file {config_file}: {e}", RNS.LOG_ERROR)
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
class PageNode:
|
class PageNode:
|
||||||
def __init__(self, identity, pagespath, filespath, announce_interval=360, name=None, page_refresh_interval=0, file_refresh_interval=0):
|
"""A Reticulum page node that serves .mu pages and files over RNS."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
identity,
|
||||||
|
pagespath,
|
||||||
|
filespath,
|
||||||
|
announce_interval=360,
|
||||||
|
name=None,
|
||||||
|
page_refresh_interval=0,
|
||||||
|
file_refresh_interval=0,
|
||||||
|
):
|
||||||
|
"""Initialize the PageNode.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
identity: RNS Identity for the node
|
||||||
|
pagespath: Path to directory containing .mu pages
|
||||||
|
filespath: Path to directory containing files to serve
|
||||||
|
announce_interval: Minutes between announcements (default: 360) == 6 hours
|
||||||
|
name: Display name for the node (optional)
|
||||||
|
page_refresh_interval: Seconds between page rescans (0 = disabled)
|
||||||
|
file_refresh_interval: Seconds between file rescans (0 = disabled)
|
||||||
|
|
||||||
|
"""
|
||||||
|
self._stop_event = threading.Event()
|
||||||
|
self._lock = threading.Lock()
|
||||||
self.identity = identity
|
self.identity = identity
|
||||||
self.name = name
|
self.name = name
|
||||||
self.pagespath = pagespath
|
self.pagespath = pagespath
|
||||||
@@ -32,7 +100,7 @@ class PageNode:
|
|||||||
RNS.Destination.IN,
|
RNS.Destination.IN,
|
||||||
RNS.Destination.SINGLE,
|
RNS.Destination.SINGLE,
|
||||||
"nomadnetwork",
|
"nomadnetwork",
|
||||||
"node"
|
"node",
|
||||||
)
|
)
|
||||||
self.announce_interval = announce_interval
|
self.announce_interval = announce_interval
|
||||||
self.last_announce = 0
|
self.last_announce = 0
|
||||||
@@ -46,154 +114,465 @@ class PageNode:
|
|||||||
|
|
||||||
self.destination.set_link_established_callback(self.on_connect)
|
self.destination.set_link_established_callback(self.on_connect)
|
||||||
|
|
||||||
threading.Thread(target=self._announce_loop, daemon=True).start()
|
self._announce_thread = threading.Thread(
|
||||||
threading.Thread(target=self._refresh_loop, daemon=True).start()
|
target=self._announce_loop,
|
||||||
|
daemon=True,
|
||||||
|
)
|
||||||
|
self._announce_thread.start()
|
||||||
|
self._refresh_thread = threading.Thread(target=self._refresh_loop, daemon=True)
|
||||||
|
self._refresh_thread.start()
|
||||||
|
|
||||||
def register_pages(self):
|
def register_pages(self):
|
||||||
self.servedpages = []
|
"""Scan pages directory and register request handlers for all .mu files."""
|
||||||
self._scan_pages(self.pagespath)
|
pages = self._scan_pages(self.pagespath)
|
||||||
|
|
||||||
if not os.path.isfile(os.path.join(self.pagespath, "index.mu")):
|
with self._lock:
|
||||||
|
self.servedpages = pages
|
||||||
|
|
||||||
|
pagespath = Path(self.pagespath).resolve()
|
||||||
|
|
||||||
|
if not (pagespath / "index.mu").is_file():
|
||||||
self.destination.register_request_handler(
|
self.destination.register_request_handler(
|
||||||
"/page/index.mu",
|
"/page/index.mu",
|
||||||
response_generator=self.serve_default_index,
|
response_generator=self.serve_default_index,
|
||||||
allow=RNS.Destination.ALLOW_ALL
|
allow=RNS.Destination.ALLOW_ALL,
|
||||||
)
|
)
|
||||||
|
|
||||||
for full_path in self.servedpages:
|
for full_path in pages:
|
||||||
rel = full_path[len(self.pagespath):]
|
page_path = Path(full_path).resolve()
|
||||||
request_path = f"/page{rel}"
|
try:
|
||||||
|
rel = page_path.relative_to(pagespath).as_posix()
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
request_path = f"/page/{rel}"
|
||||||
self.destination.register_request_handler(
|
self.destination.register_request_handler(
|
||||||
request_path,
|
request_path,
|
||||||
response_generator=self.serve_page,
|
response_generator=self.serve_page,
|
||||||
allow=RNS.Destination.ALLOW_ALL
|
allow=RNS.Destination.ALLOW_ALL,
|
||||||
)
|
)
|
||||||
|
|
||||||
def register_files(self):
|
def register_files(self):
|
||||||
self.servedfiles = []
|
"""Scan files directory and register request handlers for all files."""
|
||||||
self._scan_files(self.filespath)
|
files = self._scan_files(self.filespath)
|
||||||
|
|
||||||
for full_path in self.servedfiles:
|
with self._lock:
|
||||||
rel = full_path[len(self.filespath):]
|
self.servedfiles = files
|
||||||
request_path = f"/file{rel}"
|
|
||||||
|
filespath = Path(self.filespath).resolve()
|
||||||
|
|
||||||
|
for full_path in files:
|
||||||
|
file_path = Path(full_path).resolve()
|
||||||
|
try:
|
||||||
|
rel = file_path.relative_to(filespath).as_posix()
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
request_path = f"/file/{rel}"
|
||||||
self.destination.register_request_handler(
|
self.destination.register_request_handler(
|
||||||
request_path,
|
request_path,
|
||||||
response_generator=self.serve_file,
|
response_generator=self.serve_file,
|
||||||
allow=RNS.Destination.ALLOW_ALL,
|
allow=RNS.Destination.ALLOW_ALL,
|
||||||
auto_compress=32_000_000
|
auto_compress=32_000_000,
|
||||||
)
|
)
|
||||||
|
|
||||||
def _scan_pages(self, base):
|
def _scan_pages(self, base):
|
||||||
for entry in os.listdir(base):
|
"""Return a list of page paths under the given directory, excluding .allowed files."""
|
||||||
if entry.startswith('.'):
|
base_path = Path(base)
|
||||||
|
if not base_path.exists():
|
||||||
|
return []
|
||||||
|
served = []
|
||||||
|
for entry in base_path.iterdir():
|
||||||
|
if entry.name.startswith("."):
|
||||||
continue
|
continue
|
||||||
path = os.path.join(base, entry)
|
if entry.is_dir():
|
||||||
if os.path.isdir(path):
|
served.extend(self._scan_pages(entry))
|
||||||
self._scan_pages(path)
|
elif entry.is_file() and not entry.name.endswith(".allowed"):
|
||||||
elif os.path.isfile(path) and not entry.endswith(".allowed"):
|
served.append(str(entry))
|
||||||
self.servedpages.append(path)
|
return served
|
||||||
|
|
||||||
def _scan_files(self, base):
|
def _scan_files(self, base):
|
||||||
for entry in os.listdir(base):
|
"""Return all file paths under the given directory."""
|
||||||
if entry.startswith('.'):
|
base_path = Path(base)
|
||||||
|
if not base_path.exists():
|
||||||
|
return []
|
||||||
|
served = []
|
||||||
|
for entry in base_path.iterdir():
|
||||||
|
if entry.name.startswith("."):
|
||||||
continue
|
continue
|
||||||
path = os.path.join(base, entry)
|
if entry.is_dir():
|
||||||
if os.path.isdir(path):
|
served.extend(self._scan_files(entry))
|
||||||
self._scan_files(path)
|
elif entry.is_file():
|
||||||
elif os.path.isfile(path):
|
served.append(str(entry))
|
||||||
self.servedfiles.append(path)
|
return served
|
||||||
|
|
||||||
def serve_default_index(self, path, data, request_id, link_id, remote_identity, requested_at):
|
@staticmethod
|
||||||
return DEFAULT_INDEX.encode('utf-8')
|
def serve_default_index(
|
||||||
|
_path,
|
||||||
|
_data,
|
||||||
|
_request_id,
|
||||||
|
_link_id,
|
||||||
|
_remote_identity,
|
||||||
|
_requested_at,
|
||||||
|
):
|
||||||
|
"""Serve the default index page when no index.mu file exists."""
|
||||||
|
return DEFAULT_INDEX.encode("utf-8")
|
||||||
|
|
||||||
def serve_page(self, path, data, request_id, link_id, remote_identity, requested_at):
|
def serve_page(
|
||||||
file_path = path.replace("/page", self.pagespath, 1)
|
self,
|
||||||
|
path,
|
||||||
|
data,
|
||||||
|
_request_id,
|
||||||
|
_link_id,
|
||||||
|
remote_identity,
|
||||||
|
_requested_at,
|
||||||
|
):
|
||||||
|
"""Serve a .mu page file, executing it as a script if it has a shebang."""
|
||||||
|
pagespath = Path(self.pagespath).resolve()
|
||||||
|
relative_path = path[6:] if path.startswith("/page/") else path[5:]
|
||||||
|
file_path = (pagespath / relative_path).resolve()
|
||||||
|
|
||||||
|
if not str(file_path).startswith(str(pagespath)):
|
||||||
|
return DEFAULT_NOTALLOWED.encode("utf-8")
|
||||||
|
is_script = False
|
||||||
|
file_content = None
|
||||||
try:
|
try:
|
||||||
with open(file_path, 'rb') as _f:
|
with file_path.open("rb") as file_handle:
|
||||||
first_line = _f.readline()
|
first_line = file_handle.readline()
|
||||||
is_script = first_line.startswith(b'#!')
|
is_script = first_line.startswith(b"#!")
|
||||||
except Exception:
|
file_handle.seek(0)
|
||||||
is_script = False
|
if not is_script:
|
||||||
if is_script and os.access(file_path, os.X_OK):
|
return file_handle.read()
|
||||||
# Note: You can remove the following try-except block and just serve the page content statically
|
file_content = file_handle.read()
|
||||||
try:
|
except FileNotFoundError:
|
||||||
result = subprocess.run([file_path], stdout=subprocess.PIPE)
|
return DEFAULT_NOTALLOWED.encode("utf-8")
|
||||||
return result.stdout
|
except OSError as err:
|
||||||
except Exception:
|
RNS.log(f"Error reading page {file_path}: {err}", RNS.LOG_ERROR)
|
||||||
pass
|
return DEFAULT_NOTALLOWED.encode("utf-8")
|
||||||
with open(file_path, 'rb') as f:
|
|
||||||
return f.read()
|
|
||||||
|
|
||||||
def serve_file(self, path, data, request_id, link_id, remote_identity, requested_at):
|
if is_script and os.access(str(file_path), os.X_OK):
|
||||||
file_path = path.replace("/file", self.filespath, 1)
|
try:
|
||||||
return [open(file_path, 'rb'), {"name": os.path.basename(file_path).encode('utf-8')}]
|
env_map = os.environ.copy()
|
||||||
|
if _link_id is not None:
|
||||||
|
env_map["link_id"] = RNS.hexrep(_link_id, delimit=False)
|
||||||
|
if remote_identity is not None:
|
||||||
|
env_map["remote_identity"] = RNS.hexrep(
|
||||||
|
remote_identity.hash,
|
||||||
|
delimit=False,
|
||||||
|
)
|
||||||
|
if data is not None and isinstance(data, dict):
|
||||||
|
for e in data:
|
||||||
|
if isinstance(e, str) and (
|
||||||
|
e.startswith("field_") or e.startswith("var_")
|
||||||
|
):
|
||||||
|
env_map[e] = data[e]
|
||||||
|
result = subprocess.run( # noqa: S603
|
||||||
|
[str(file_path)],
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
check=True,
|
||||||
|
env=env_map,
|
||||||
|
)
|
||||||
|
return result.stdout
|
||||||
|
except Exception as e:
|
||||||
|
RNS.log(f"Error executing script page: {e}", RNS.LOG_ERROR)
|
||||||
|
if file_content is not None:
|
||||||
|
return file_content
|
||||||
|
try:
|
||||||
|
return self._read_file_bytes(file_path)
|
||||||
|
except FileNotFoundError:
|
||||||
|
return DEFAULT_NOTALLOWED.encode("utf-8")
|
||||||
|
except OSError as err:
|
||||||
|
RNS.log(f"Error reading page fallback {file_path}: {err}", RNS.LOG_ERROR)
|
||||||
|
return DEFAULT_NOTALLOWED.encode("utf-8")
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _read_file_bytes(file_path):
|
||||||
|
"""Read a file's bytes and return the contents."""
|
||||||
|
with file_path.open("rb") as file_handle:
|
||||||
|
return file_handle.read()
|
||||||
|
|
||||||
|
def serve_file(
|
||||||
|
self,
|
||||||
|
path,
|
||||||
|
_data,
|
||||||
|
_request_id,
|
||||||
|
_link_id,
|
||||||
|
_remote_identity,
|
||||||
|
_requested_at,
|
||||||
|
):
|
||||||
|
"""Serve a file from the files directory."""
|
||||||
|
filespath = Path(self.filespath).resolve()
|
||||||
|
relative_path = path[6:] if path.startswith("/file/") else path[5:]
|
||||||
|
file_path = (filespath / relative_path).resolve()
|
||||||
|
|
||||||
|
if not str(file_path).startswith(str(filespath)):
|
||||||
|
return DEFAULT_NOTALLOWED.encode("utf-8")
|
||||||
|
|
||||||
|
return [
|
||||||
|
file_path.open("rb"),
|
||||||
|
{"name": file_path.name.encode("utf-8")},
|
||||||
|
]
|
||||||
|
|
||||||
def on_connect(self, link):
|
def on_connect(self, link):
|
||||||
pass
|
"""Handle new link connections."""
|
||||||
|
|
||||||
def _announce_loop(self):
|
def _announce_loop(self):
|
||||||
while True:
|
"""Periodically announce the node until shutdown is requested."""
|
||||||
if time.time() - self.last_announce > self.announce_interval:
|
interval_seconds = max(self.announce_interval, 0) * 60
|
||||||
if self.name:
|
try:
|
||||||
self.destination.announce(app_data=self.name.encode('utf-8'))
|
while not self._stop_event.is_set():
|
||||||
else:
|
now = time.time()
|
||||||
self.destination.announce()
|
if (
|
||||||
self.last_announce = time.time()
|
self.last_announce == 0
|
||||||
time.sleep(1)
|
or now - self.last_announce >= interval_seconds
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
if self.name:
|
||||||
|
self.destination.announce(
|
||||||
|
app_data=self.name.encode("utf-8"),
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.destination.announce()
|
||||||
|
self.last_announce = time.time()
|
||||||
|
except (TypeError, ValueError) as announce_error:
|
||||||
|
RNS.log(
|
||||||
|
f"Error during announce: {announce_error}",
|
||||||
|
RNS.LOG_ERROR,
|
||||||
|
)
|
||||||
|
wait_time = max(
|
||||||
|
(self.last_announce + interval_seconds) - time.time()
|
||||||
|
if self.last_announce
|
||||||
|
else 0,
|
||||||
|
1,
|
||||||
|
)
|
||||||
|
self._stop_event.wait(min(wait_time, 60))
|
||||||
|
except Exception as e:
|
||||||
|
RNS.log(f"Error in announce loop: {e}", RNS.LOG_ERROR)
|
||||||
|
|
||||||
def _refresh_loop(self):
|
def _refresh_loop(self):
|
||||||
while True:
|
"""Refresh page and file registrations at configured intervals."""
|
||||||
now = time.time()
|
try:
|
||||||
if self.page_refresh_interval > 0 and now - self.last_page_refresh > self.page_refresh_interval:
|
while not self._stop_event.is_set():
|
||||||
self.register_pages()
|
now = time.time()
|
||||||
self.last_page_refresh = now
|
if (
|
||||||
if self.file_refresh_interval > 0 and now - self.last_file_refresh > self.file_refresh_interval:
|
self.page_refresh_interval > 0
|
||||||
self.register_files()
|
and now - self.last_page_refresh >= self.page_refresh_interval
|
||||||
self.last_file_refresh = now
|
):
|
||||||
time.sleep(1)
|
self.register_pages()
|
||||||
|
self.last_page_refresh = time.time()
|
||||||
|
if (
|
||||||
|
self.file_refresh_interval > 0
|
||||||
|
and now - self.last_file_refresh >= self.file_refresh_interval
|
||||||
|
):
|
||||||
|
self.register_files()
|
||||||
|
self.last_file_refresh = time.time()
|
||||||
|
|
||||||
|
wait_candidates = []
|
||||||
|
if self.page_refresh_interval > 0:
|
||||||
|
wait_candidates.append(
|
||||||
|
max(
|
||||||
|
(self.last_page_refresh + self.page_refresh_interval)
|
||||||
|
- time.time(),
|
||||||
|
0.5,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
if self.file_refresh_interval > 0:
|
||||||
|
wait_candidates.append(
|
||||||
|
max(
|
||||||
|
(self.last_file_refresh + self.file_refresh_interval)
|
||||||
|
- time.time(),
|
||||||
|
0.5,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
wait_time = min(wait_candidates) if wait_candidates else 1.0
|
||||||
|
self._stop_event.wait(min(wait_time, 60))
|
||||||
|
except Exception as e:
|
||||||
|
RNS.log(f"Error in refresh loop: {e}", RNS.LOG_ERROR)
|
||||||
|
|
||||||
|
def shutdown(self):
|
||||||
|
"""Gracefully shutdown the PageNode and cleanup resources."""
|
||||||
|
RNS.log("Shutting down PageNode...", RNS.LOG_INFO)
|
||||||
|
self._stop_event.set()
|
||||||
|
try:
|
||||||
|
self._announce_thread.join(timeout=5)
|
||||||
|
self._refresh_thread.join(timeout=5)
|
||||||
|
except Exception as e:
|
||||||
|
RNS.log(f"Error waiting for threads to shut down: {e}", RNS.LOG_ERROR)
|
||||||
|
try:
|
||||||
|
if hasattr(self.destination, "close"):
|
||||||
|
self.destination.close()
|
||||||
|
except Exception as e:
|
||||||
|
RNS.log(f"Error closing RNS destination: {e}", RNS.LOG_ERROR)
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
"""Run the RNS page node application."""
|
||||||
parser = argparse.ArgumentParser(description="Minimal Reticulum Page Node")
|
parser = argparse.ArgumentParser(description="Minimal Reticulum Page Node")
|
||||||
parser.add_argument('-c', '--config', dest='configpath', help='Reticulum config path', default=None)
|
parser.add_argument(
|
||||||
parser.add_argument('-p', '--pages-dir', dest='pages_dir', help='Pages directory', default=os.path.join(os.getcwd(), 'pages'))
|
"node_config",
|
||||||
parser.add_argument('-f', '--files-dir', dest='files_dir', help='Files directory', default=os.path.join(os.getcwd(), 'files'))
|
nargs="?",
|
||||||
parser.add_argument('-n', '--node-name', dest='node_name', help='Node display name', default=None)
|
help="Path to rns-page-node config file",
|
||||||
parser.add_argument('-a', '--announce-interval', dest='announce_interval', type=int, help='Announce interval in seconds', default=360)
|
default=None,
|
||||||
parser.add_argument('-i', '--identity-dir', dest='identity_dir', help='Directory to store node identity', default=os.path.join(os.getcwd(), 'node-config'))
|
)
|
||||||
parser.add_argument('--page-refresh-interval', dest='page_refresh_interval', type=int, default=0, help='Page refresh interval in seconds, 0 disables auto-refresh')
|
parser.add_argument(
|
||||||
parser.add_argument('--file-refresh-interval', dest='file_refresh_interval', type=int, default=0, help='File refresh interval in seconds, 0 disables auto-refresh')
|
"-c",
|
||||||
|
"--config",
|
||||||
|
dest="configpath",
|
||||||
|
help="Reticulum config path",
|
||||||
|
default=None,
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-p",
|
||||||
|
"--pages-dir",
|
||||||
|
dest="pages_dir",
|
||||||
|
help="Pages directory",
|
||||||
|
default=str(Path.cwd() / "pages"),
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-f",
|
||||||
|
"--files-dir",
|
||||||
|
dest="files_dir",
|
||||||
|
help="Files directory",
|
||||||
|
default=str(Path.cwd() / "files"),
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-n",
|
||||||
|
"--node-name",
|
||||||
|
dest="node_name",
|
||||||
|
help="Node display name",
|
||||||
|
default=None,
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-a",
|
||||||
|
"--announce-interval",
|
||||||
|
dest="announce_interval",
|
||||||
|
type=int,
|
||||||
|
help="Announce interval in minutes",
|
||||||
|
default=360,
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-i",
|
||||||
|
"--identity-dir",
|
||||||
|
dest="identity_dir",
|
||||||
|
help="Directory to store node identity",
|
||||||
|
default=str(Path.cwd() / "node-config"),
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--page-refresh-interval",
|
||||||
|
dest="page_refresh_interval",
|
||||||
|
type=int,
|
||||||
|
default=0,
|
||||||
|
help="Page refresh interval in seconds, 0 disables auto-refresh",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--file-refresh-interval",
|
||||||
|
dest="file_refresh_interval",
|
||||||
|
type=int,
|
||||||
|
default=0,
|
||||||
|
help="File refresh interval in seconds, 0 disables auto-refresh",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"-l",
|
||||||
|
"--log-level",
|
||||||
|
dest="log_level",
|
||||||
|
choices=["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"],
|
||||||
|
default="INFO",
|
||||||
|
help="Logging level",
|
||||||
|
)
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
configpath = args.configpath
|
config = {}
|
||||||
pages_dir = args.pages_dir
|
if args.node_config:
|
||||||
files_dir = args.files_dir
|
config = load_config(args.node_config)
|
||||||
node_name = args.node_name
|
|
||||||
announce_interval = args.announce_interval
|
def get_config_value(arg_value, arg_default, config_key, value_type=str):
|
||||||
identity_dir = args.identity_dir
|
"""Get value from CLI args, config file, or default.
|
||||||
page_refresh_interval = args.page_refresh_interval
|
|
||||||
file_refresh_interval = args.file_refresh_interval
|
Priority: CLI arg > config file > default
|
||||||
|
"""
|
||||||
|
if arg_value != arg_default:
|
||||||
|
return arg_value
|
||||||
|
if config_key in config:
|
||||||
|
try:
|
||||||
|
if value_type is int:
|
||||||
|
return int(config[config_key])
|
||||||
|
return config[config_key]
|
||||||
|
except ValueError:
|
||||||
|
RNS.log(
|
||||||
|
f"Invalid {value_type.__name__} value for {config_key}: {config[config_key]}",
|
||||||
|
RNS.LOG_WARNING,
|
||||||
|
)
|
||||||
|
return arg_default
|
||||||
|
|
||||||
|
configpath = get_config_value(args.configpath, None, "reticulum-config")
|
||||||
|
pages_dir = get_config_value(args.pages_dir, str(Path.cwd() / "pages"), "pages-dir")
|
||||||
|
files_dir = get_config_value(args.files_dir, str(Path.cwd() / "files"), "files-dir")
|
||||||
|
node_name = get_config_value(args.node_name, None, "node-name")
|
||||||
|
announce_interval = get_config_value(
|
||||||
|
args.announce_interval,
|
||||||
|
360,
|
||||||
|
"announce-interval",
|
||||||
|
int,
|
||||||
|
)
|
||||||
|
identity_dir = get_config_value(
|
||||||
|
args.identity_dir,
|
||||||
|
str(Path.cwd() / "node-config"),
|
||||||
|
"identity-dir",
|
||||||
|
)
|
||||||
|
page_refresh_interval = get_config_value(
|
||||||
|
args.page_refresh_interval,
|
||||||
|
0,
|
||||||
|
"page-refresh-interval",
|
||||||
|
int,
|
||||||
|
)
|
||||||
|
file_refresh_interval = get_config_value(
|
||||||
|
args.file_refresh_interval,
|
||||||
|
0,
|
||||||
|
"file-refresh-interval",
|
||||||
|
int,
|
||||||
|
)
|
||||||
|
log_level = get_config_value(args.log_level, "INFO", "log-level")
|
||||||
|
|
||||||
|
# Set RNS log level based on command line argument
|
||||||
|
log_level_map = {
|
||||||
|
"CRITICAL": RNS.LOG_CRITICAL,
|
||||||
|
"ERROR": RNS.LOG_ERROR,
|
||||||
|
"WARNING": RNS.LOG_WARNING,
|
||||||
|
"INFO": RNS.LOG_INFO,
|
||||||
|
"DEBUG": RNS.LOG_DEBUG,
|
||||||
|
}
|
||||||
|
RNS.loglevel = log_level_map.get(log_level.upper(), RNS.LOG_INFO)
|
||||||
|
|
||||||
RNS.Reticulum(configpath)
|
RNS.Reticulum(configpath)
|
||||||
os.makedirs(identity_dir, exist_ok=True)
|
Path(identity_dir).mkdir(parents=True, exist_ok=True)
|
||||||
identity_file = os.path.join(identity_dir, 'identity')
|
identity_file = Path(identity_dir) / "identity"
|
||||||
if os.path.isfile(identity_file):
|
if identity_file.is_file():
|
||||||
identity = RNS.Identity.from_file(identity_file)
|
identity = RNS.Identity.from_file(str(identity_file))
|
||||||
else:
|
else:
|
||||||
identity = RNS.Identity()
|
identity = RNS.Identity()
|
||||||
identity.to_file(identity_file)
|
identity.to_file(str(identity_file))
|
||||||
|
|
||||||
os.makedirs(pages_dir, exist_ok=True)
|
Path(pages_dir).mkdir(parents=True, exist_ok=True)
|
||||||
os.makedirs(files_dir, exist_ok=True)
|
Path(files_dir).mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
node = PageNode(identity, pages_dir, files_dir, announce_interval, node_name, page_refresh_interval, file_refresh_interval)
|
node = PageNode(
|
||||||
print("Page node running. Press Ctrl-C to exit.")
|
identity,
|
||||||
|
pages_dir,
|
||||||
|
files_dir,
|
||||||
|
announce_interval,
|
||||||
|
node_name,
|
||||||
|
page_refresh_interval,
|
||||||
|
file_refresh_interval,
|
||||||
|
)
|
||||||
|
RNS.log("Page node running. Press Ctrl-C to exit.", RNS.LOG_INFO)
|
||||||
|
RNS.log(f"Node address: {RNS.prettyhexrep(node.destination.hash)}", RNS.LOG_INFO)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
while True:
|
while True:
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
print("Shutting down.")
|
RNS.log("Keyboard interrupt received, shutting down...", RNS.LOG_INFO)
|
||||||
|
node.shutdown()
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|||||||
35
setup.py
35
setup.py
@@ -1,31 +1,28 @@
|
|||||||
from setuptools import setup, find_packages
|
from setuptools import setup, find_packages
|
||||||
|
|
||||||
with open('README.md', 'r', encoding='utf-8') as fh:
|
|
||||||
long_description = fh.read()
|
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
name='rns-page-node',
|
name="rns-page-node",
|
||||||
version='0.1.0',
|
version="1.3.0",
|
||||||
author='Sudo-Ivan',
|
description="A simple way to serve pages and files over the Reticulum network.",
|
||||||
author_email='',
|
long_description=open("README.md").read(),
|
||||||
description='A simple way to serve pages and files over the Reticulum network.',
|
long_description_content_type="text/markdown",
|
||||||
long_description=long_description,
|
author="Sudo-Ivan",
|
||||||
long_description_content_type='text/markdown',
|
url="https://git.quad4.io/RNS-Things/rns-page-node",
|
||||||
url='https://github.com/Sudo-Ivan/rns-page-node',
|
|
||||||
packages=find_packages(),
|
packages=find_packages(),
|
||||||
python_requires='>=3.9',
|
|
||||||
install_requires=[
|
install_requires=[
|
||||||
'rns>=0.9.6,<0.10.0',
|
"rns>=1.0.4,<1.5.0",
|
||||||
|
"cryptography>=46.0.3",
|
||||||
],
|
],
|
||||||
entry_points={
|
entry_points={
|
||||||
'console_scripts': [
|
"console_scripts": [
|
||||||
'rns-page-node=rns_page_node.main:main',
|
"rns-page-node=rns_page_node.main:main",
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
license='GPL-3.0',
|
|
||||||
classifiers=[
|
classifiers=[
|
||||||
'Programming Language :: Python :: 3',
|
"Programming Language :: Python :: 3",
|
||||||
'License :: OSI Approved :: GNU General Public License v3 (GPLv3)',
|
"Operating System :: OS Independent",
|
||||||
'Operating System :: OS Independent',
|
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
|
||||||
],
|
],
|
||||||
|
python_requires=">=3.9.2",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
4
tests/.gitignore
vendored
Normal file
4
tests/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
pages/
|
||||||
|
node-config/
|
||||||
|
node.log
|
||||||
|
config/
|
||||||
66
tests/run_tests.sh
Executable file
66
tests/run_tests.sh
Executable file
@@ -0,0 +1,66 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
set -e
|
||||||
|
cd "$(dirname "${BASH_SOURCE[0]}")"
|
||||||
|
|
||||||
|
# Remove previous test artifacts
|
||||||
|
rm -rf config node-config pages files node.log
|
||||||
|
|
||||||
|
# Create directories for config, node identity, pages, and files
|
||||||
|
mkdir -p config node-config pages files
|
||||||
|
|
||||||
|
# Create a sample page and a test file
|
||||||
|
cat > pages/index.mu << 'EOF'
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
import os
|
||||||
|
|
||||||
|
print("`F0f0`_`Test Page`_")
|
||||||
|
print("This is a test page with environment variable support.")
|
||||||
|
print()
|
||||||
|
|
||||||
|
print("`F0f0`_`Environment Variables`_")
|
||||||
|
params = []
|
||||||
|
for key, value in os.environ.items():
|
||||||
|
if key.startswith(('field_', 'var_')):
|
||||||
|
params.append(f"- `Faaa`{key}`f: `F0f0`{value}`f")
|
||||||
|
|
||||||
|
if params:
|
||||||
|
print("\n".join(params))
|
||||||
|
else:
|
||||||
|
print("- No parameters received")
|
||||||
|
|
||||||
|
print()
|
||||||
|
print("`F0f0`_`Remote Identity`_")
|
||||||
|
remote_id = os.environ.get('remote_identity', '33aff86b736acd47dca07e84630fd192') # Mock for testing
|
||||||
|
print(f"`Faaa`{remote_id}`f")
|
||||||
|
EOF
|
||||||
|
|
||||||
|
chmod +x pages/index.mu
|
||||||
|
|
||||||
|
cat > files/text.txt << EOF
|
||||||
|
This is a test file.
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Start the page node in the background
|
||||||
|
python3 ../rns_page_node/main.py -c config -i node-config -p pages -f files > node.log 2>&1 &
|
||||||
|
NODE_PID=$!
|
||||||
|
|
||||||
|
# Wait for node to generate its identity file
|
||||||
|
echo "Waiting for node identity..."
|
||||||
|
for i in {1..40}; do
|
||||||
|
if [ -f node-config/identity ]; then
|
||||||
|
echo "Identity file found"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
sleep 0.25
|
||||||
|
done
|
||||||
|
if [ ! -f node-config/identity ]; then
|
||||||
|
echo "Error: node identity file not found" >&2
|
||||||
|
kill $NODE_PID
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Run the client test
|
||||||
|
python3 test_client.py
|
||||||
|
|
||||||
|
# Clean up
|
||||||
|
kill $NODE_PID
|
||||||
229
tests/test_client.py
Normal file
229
tests/test_client.py
Normal file
@@ -0,0 +1,229 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
|
||||||
|
import RNS
|
||||||
|
|
||||||
|
# Determine base directory for tests
|
||||||
|
dir_path = os.path.abspath(os.path.dirname(__file__))
|
||||||
|
config_dir = os.path.join(dir_path, "config")
|
||||||
|
identity_dir = os.path.join(dir_path, "node-config")
|
||||||
|
|
||||||
|
# Initialize Reticulum with shared config
|
||||||
|
RNS.Reticulum(config_dir)
|
||||||
|
|
||||||
|
# Load server identity (created by the page node)
|
||||||
|
identity_file = os.path.join(identity_dir, "identity")
|
||||||
|
server_identity = RNS.Identity.from_file(identity_file)
|
||||||
|
|
||||||
|
# Create a destination to the server node
|
||||||
|
destination = RNS.Destination(
|
||||||
|
server_identity,
|
||||||
|
RNS.Destination.OUT,
|
||||||
|
RNS.Destination.SINGLE,
|
||||||
|
"nomadnetwork",
|
||||||
|
"node",
|
||||||
|
)
|
||||||
|
|
||||||
|
# Ensure we know a path to the destination
|
||||||
|
if not RNS.Transport.has_path(destination.hash):
|
||||||
|
RNS.Transport.request_path(destination.hash)
|
||||||
|
while not RNS.Transport.has_path(destination.hash):
|
||||||
|
time.sleep(0.1)
|
||||||
|
|
||||||
|
# Establish a link to the server
|
||||||
|
global_link = RNS.Link(destination)
|
||||||
|
|
||||||
|
# Containers for responses
|
||||||
|
responses = {}
|
||||||
|
done_event = threading.Event()
|
||||||
|
|
||||||
|
# Test data for environment variables
|
||||||
|
test_data_dict = {
|
||||||
|
"var_field_test": "dictionary_value",
|
||||||
|
"var_field_message": "hello_world",
|
||||||
|
"var_action": "test_action",
|
||||||
|
}
|
||||||
|
test_data_dict2 = {
|
||||||
|
"field_username": "testuser",
|
||||||
|
"field_message": "hello_from_form",
|
||||||
|
"var_action": "submit",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Callback for page response
|
||||||
|
def on_page(response):
|
||||||
|
data = response.response
|
||||||
|
if isinstance(data, bytes):
|
||||||
|
text = data.decode("utf-8")
|
||||||
|
else:
|
||||||
|
text = str(data)
|
||||||
|
print("Received page (no data):")
|
||||||
|
print(text)
|
||||||
|
responses["page"] = text
|
||||||
|
check_responses()
|
||||||
|
|
||||||
|
|
||||||
|
# Callback for page response with dictionary data
|
||||||
|
def on_page_dict(response):
|
||||||
|
data = response.response
|
||||||
|
if isinstance(data, bytes):
|
||||||
|
text = data.decode("utf-8")
|
||||||
|
else:
|
||||||
|
text = str(data)
|
||||||
|
print("Received page (dict data):")
|
||||||
|
print(text)
|
||||||
|
responses["page_dict"] = text
|
||||||
|
check_responses()
|
||||||
|
|
||||||
|
|
||||||
|
# Callback for page response with second dict data
|
||||||
|
def on_page_dict2(response):
|
||||||
|
data = response.response
|
||||||
|
if isinstance(data, bytes):
|
||||||
|
text = data.decode("utf-8")
|
||||||
|
else:
|
||||||
|
text = str(data)
|
||||||
|
print("Received page (dict2 data):")
|
||||||
|
print(text)
|
||||||
|
responses["page_dict2"] = text
|
||||||
|
check_responses()
|
||||||
|
|
||||||
|
|
||||||
|
def check_responses():
|
||||||
|
if (
|
||||||
|
"page" in responses
|
||||||
|
and "page_dict" in responses
|
||||||
|
and "page_dict2" in responses
|
||||||
|
and "file" in responses
|
||||||
|
):
|
||||||
|
done_event.set()
|
||||||
|
|
||||||
|
|
||||||
|
# Callback for file response
|
||||||
|
def on_file(response):
|
||||||
|
data = response.response
|
||||||
|
# Handle response as [fileobj, headers]
|
||||||
|
if isinstance(data, list) and len(data) == 2 and hasattr(data[0], "read"):
|
||||||
|
fileobj, headers = data
|
||||||
|
file_data = fileobj.read()
|
||||||
|
filename = headers.get(b"name", b"").decode("utf-8")
|
||||||
|
print(f"Received file ({filename}):")
|
||||||
|
print(file_data.decode("utf-8"))
|
||||||
|
responses["file"] = file_data.decode("utf-8")
|
||||||
|
# Handle response as a raw file object
|
||||||
|
elif hasattr(data, "read"):
|
||||||
|
file_data = data.read()
|
||||||
|
filename = os.path.basename("text.txt")
|
||||||
|
print(f"Received file ({filename}):")
|
||||||
|
print(file_data.decode("utf-8"))
|
||||||
|
responses["file"] = file_data.decode("utf-8")
|
||||||
|
# Handle response as raw bytes
|
||||||
|
elif isinstance(data, bytes):
|
||||||
|
text = data.decode("utf-8")
|
||||||
|
print("Received file:")
|
||||||
|
print(text)
|
||||||
|
responses["file"] = text
|
||||||
|
else:
|
||||||
|
print("Received file (unhandled format):", data)
|
||||||
|
responses["file"] = str(data)
|
||||||
|
check_responses()
|
||||||
|
|
||||||
|
|
||||||
|
# Request the pages and file once the link is established
|
||||||
|
def on_link_established(link):
|
||||||
|
# Test page without data
|
||||||
|
link.request("/page/index.mu", None, response_callback=on_page)
|
||||||
|
# Test page with dictionary data (simulates var_ prefixed data)
|
||||||
|
link.request("/page/index.mu", test_data_dict, response_callback=on_page_dict)
|
||||||
|
# Test page with form field data (simulates field_ prefixed data)
|
||||||
|
link.request("/page/index.mu", test_data_dict2, response_callback=on_page_dict2)
|
||||||
|
# Test file serving
|
||||||
|
link.request("/file/text.txt", None, response_callback=on_file)
|
||||||
|
|
||||||
|
|
||||||
|
# Register callbacks
|
||||||
|
global_link.set_link_established_callback(on_link_established)
|
||||||
|
global_link.set_link_closed_callback(lambda link: done_event.set())
|
||||||
|
|
||||||
|
# Wait for responses or timeout
|
||||||
|
if not done_event.wait(timeout=30):
|
||||||
|
print("Test timed out.", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
# Validate test results
|
||||||
|
def validate_test_results():
|
||||||
|
"""Validate that all responses contain expected content"""
|
||||||
|
# Check basic page response (no data)
|
||||||
|
if "page" not in responses:
|
||||||
|
print("ERROR: No basic page response received", file=sys.stderr)
|
||||||
|
return False
|
||||||
|
|
||||||
|
page_content = responses["page"]
|
||||||
|
if "No parameters received" not in page_content:
|
||||||
|
print("ERROR: Basic page should show 'No parameters received'", file=sys.stderr)
|
||||||
|
return False
|
||||||
|
if "33aff86b736acd47dca07e84630fd192" not in page_content:
|
||||||
|
print("ERROR: Basic page should show mock remote identity", file=sys.stderr)
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Check page with dictionary data
|
||||||
|
if "page_dict" not in responses:
|
||||||
|
print("ERROR: No dictionary data page response received", file=sys.stderr)
|
||||||
|
return False
|
||||||
|
|
||||||
|
dict_content = responses["page_dict"]
|
||||||
|
if "var_field_test" not in dict_content or "dictionary_value" not in dict_content:
|
||||||
|
print(
|
||||||
|
"ERROR: Dictionary data page should contain processed environment variables",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
if "33aff86b736acd47dca07e84630fd192" not in dict_content:
|
||||||
|
print(
|
||||||
|
"ERROR: Dictionary data page should show mock remote identity",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Check page with second dictionary data (form fields)
|
||||||
|
if "page_dict2" not in responses:
|
||||||
|
print("ERROR: No dict2 data page response received", file=sys.stderr)
|
||||||
|
return False
|
||||||
|
|
||||||
|
dict2_content = responses["page_dict2"]
|
||||||
|
if "field_username" not in dict2_content or "testuser" not in dict2_content:
|
||||||
|
print(
|
||||||
|
"ERROR: Dict2 data page should contain processed environment variables",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
if "33aff86b736acd47dca07e84630fd192" not in dict2_content:
|
||||||
|
print(
|
||||||
|
"ERROR: Dict2 data page should show mock remote identity",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Check file response
|
||||||
|
if "file" not in responses:
|
||||||
|
print("ERROR: No file response received", file=sys.stderr)
|
||||||
|
return False
|
||||||
|
|
||||||
|
file_content = responses["file"]
|
||||||
|
if "This is a test file" not in file_content:
|
||||||
|
print("ERROR: File content doesn't match expected content", file=sys.stderr)
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
if validate_test_results():
|
||||||
|
print("All tests passed! Environment variable processing works correctly.")
|
||||||
|
sys.exit(0)
|
||||||
|
else:
|
||||||
|
print("Tests failed.", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
69
tests/test_client2.py
Normal file
69
tests/test_client2.py
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
|
||||||
|
import RNS
|
||||||
|
|
||||||
|
dir_path = os.path.abspath(os.path.dirname(__file__))
|
||||||
|
config_dir = os.path.join(dir_path, "config")
|
||||||
|
|
||||||
|
RNS.Reticulum(config_dir)
|
||||||
|
|
||||||
|
DESTINATION_HEX = (
|
||||||
|
"49b2d959db8528347d0a38083aec1042" # Ivans Node that runs rns-page-node
|
||||||
|
)
|
||||||
|
|
||||||
|
dest_len = (RNS.Reticulum.TRUNCATED_HASHLENGTH // 8) * 2
|
||||||
|
if len(DESTINATION_HEX) != dest_len:
|
||||||
|
print(
|
||||||
|
f"Invalid destination length (got {len(DESTINATION_HEX)}, expected {dest_len})",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
sys.exit(1)
|
||||||
|
destination_hash = bytes.fromhex(DESTINATION_HEX)
|
||||||
|
|
||||||
|
if not RNS.Transport.has_path(destination_hash):
|
||||||
|
print("Requesting path to server...")
|
||||||
|
RNS.Transport.request_path(destination_hash)
|
||||||
|
while not RNS.Transport.has_path(destination_hash):
|
||||||
|
time.sleep(0.1)
|
||||||
|
|
||||||
|
server_identity = RNS.Identity.recall(destination_hash)
|
||||||
|
print(f"Recalled server identity for {DESTINATION_HEX}")
|
||||||
|
|
||||||
|
destination = RNS.Destination(
|
||||||
|
server_identity,
|
||||||
|
RNS.Destination.OUT,
|
||||||
|
RNS.Destination.SINGLE,
|
||||||
|
"nomadnetwork",
|
||||||
|
"node",
|
||||||
|
)
|
||||||
|
link = RNS.Link(destination)
|
||||||
|
|
||||||
|
done_event = threading.Event()
|
||||||
|
|
||||||
|
|
||||||
|
def on_page(response):
|
||||||
|
data = response.response
|
||||||
|
if isinstance(data, bytes):
|
||||||
|
text = data.decode("utf-8")
|
||||||
|
else:
|
||||||
|
text = str(data)
|
||||||
|
print("Fetched page content:")
|
||||||
|
print(text)
|
||||||
|
done_event.set()
|
||||||
|
|
||||||
|
|
||||||
|
link.set_link_established_callback(
|
||||||
|
lambda link: link.request("/page/index.mu", None, response_callback=on_page),
|
||||||
|
)
|
||||||
|
link.set_link_closed_callback(lambda link: done_event.set())
|
||||||
|
|
||||||
|
if not done_event.wait(timeout=30):
|
||||||
|
print("Timed out waiting for page", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
print("Done fetching page.")
|
||||||
|
sys.exit(0)
|
||||||
Reference in New Issue
Block a user