Compare commits
No commits in common. "9840804e40494ac7bad2dd0ab3cc4873f0bc1d0d" and "4c8f121f5829d846eaa37019c59ae6b2e9fc1d92" have entirely different histories.
9840804e40
...
4c8f121f58
28
AGENTS.md
28
AGENTS.md
@ -16,10 +16,10 @@ This is a FastAPI-based file server that serves files from a directory or ZIP ar
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Run with a directory
|
# Run with a directory
|
||||||
uv run main.py /path/to/files
|
python main.py /path/to/files
|
||||||
|
|
||||||
# Run with a ZIP archive
|
# Run with a ZIP archive
|
||||||
uv run main.py /path/to/archive.zip
|
python main.py /path/to/archive.zip
|
||||||
```
|
```
|
||||||
|
|
||||||
### Testing
|
### Testing
|
||||||
@ -46,7 +46,7 @@ pytest -k "test_name_pattern"
|
|||||||
No linting tool is currently configured. Recommended tools:
|
No linting tool is currently configured. Recommended tools:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Install ruff (fast linter)
|
# Install ruff (fast linter/formatter)
|
||||||
uv add ruff
|
uv add ruff
|
||||||
|
|
||||||
# Run linting
|
# Run linting
|
||||||
@ -54,18 +54,9 @@ ruff check .
|
|||||||
|
|
||||||
# Auto-fix issues
|
# Auto-fix issues
|
||||||
ruff check --fix .
|
ruff check --fix .
|
||||||
```
|
|
||||||
|
|
||||||
### Formatting
|
# Format code
|
||||||
|
ruff format .
|
||||||
Always format code with black after implementing every feature:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Install black
|
|
||||||
uv add black
|
|
||||||
|
|
||||||
# Format all code
|
|
||||||
uv run black .
|
|
||||||
```
|
```
|
||||||
|
|
||||||
### Type Checking
|
### Type Checking
|
||||||
@ -75,7 +66,7 @@ uv run black .
|
|||||||
uv add mypy
|
uv add mypy
|
||||||
|
|
||||||
# Run type checking
|
# Run type checking
|
||||||
uv run mypy main.py
|
mypy main.py
|
||||||
```
|
```
|
||||||
|
|
||||||
## Code Style Guidelines
|
## Code Style Guidelines
|
||||||
@ -97,13 +88,12 @@ uv run mypy main.py
|
|||||||
from mymodule import MyClass
|
from mymodule import MyClass
|
||||||
```
|
```
|
||||||
|
|
||||||
### Formatting Style
|
### Formatting
|
||||||
|
|
||||||
- Use 4 spaces for indentation (no tabs)
|
- Use 4 spaces for indentation (no tabs)
|
||||||
- Maximum line length: 88 characters (black default)
|
- Maximum line length: 100 characters
|
||||||
- Use blank lines to separate logical sections (2 blank lines between top-level definitions)
|
- Use blank lines to separate logical sections (2 blank lines between top-level definitions)
|
||||||
- One blank line between method definitions within a class
|
- One blank line between method definitions within a class
|
||||||
- **Always run `black .` after completing every feature**
|
|
||||||
|
|
||||||
### Types
|
### Types
|
||||||
|
|
||||||
@ -206,5 +196,5 @@ uv run mypy main.py
|
|||||||
### Running the Server in Development
|
### Running the Server in Development
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
uv run main ./*.zip
|
python -m uvicorn main:app --reload --host 0.0.0.0 --port 8000
|
||||||
```
|
```
|
||||||
|
|||||||
192
README.md
192
README.md
@ -1,192 +0,0 @@
|
|||||||
# Image Server
|
|
||||||
|
|
||||||
A FastAPI-based image server that serves files from directories or ZIP archives using hashed paths for secure, randomized access. The server provides a beautiful full-screen image viewing experience with navigation controls and optional auto-advance features.
|
|
||||||
|
|
||||||
## Features
|
|
||||||
|
|
||||||
- **Secure File Access**: Files are accessed via SHA-256 hashes of their paths (with salt) rather than direct file paths
|
|
||||||
- **Multiple Sources**: Supports serving from directories, individual ZIP files, or glob patterns (e.g., `*.zip`)
|
|
||||||
- **Beautiful UI**: Full-screen responsive image viewer with keyboard navigation
|
|
||||||
- **Navigation Controls**:
|
|
||||||
- Previous/Next buttons (clickable chevrons or arrow keys)
|
|
||||||
- Random access mode
|
|
||||||
- Ordered sequential access with configurable delay
|
|
||||||
- Play/pause toggle for auto-advance
|
|
||||||
- **Keyboard Shortcuts**:
|
|
||||||
- Left/Right Arrow or H/L: Navigate previous/next
|
|
||||||
- Space: Toggle play/pause
|
|
||||||
- +/= or J: Increase slide delay
|
|
||||||
- - or K: Decrease slide delay
|
|
||||||
- O: Toggle between ordered and random modes
|
|
||||||
- **MIME Type Detection**: Automatically sets correct content types for served files
|
|
||||||
- **Health Check Endpoint**: Monitor server status and indexed file count
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
### Prerequisites
|
|
||||||
|
|
||||||
- Python 3.13 or higher
|
|
||||||
- UV package manager (recommended) or pip
|
|
||||||
|
|
||||||
### Using UV (Recommended)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Clone the repository
|
|
||||||
git clone <repository-url>
|
|
||||||
cd image_server
|
|
||||||
|
|
||||||
# Install dependencies
|
|
||||||
uv sync
|
|
||||||
|
|
||||||
# Activate virtual environment
|
|
||||||
source .venv/bin/activate
|
|
||||||
```
|
|
||||||
|
|
||||||
### Using pip
|
|
||||||
|
|
||||||
```bash
|
|
||||||
pip install fastapi uvicorn
|
|
||||||
```
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
### Basic Usage
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Serve files from a directory
|
|
||||||
python main.py /path/to/your/images
|
|
||||||
|
|
||||||
# Serve files from a ZIP archive
|
|
||||||
python main.py /path/to/archive.zip
|
|
||||||
|
|
||||||
# Serve files matching a glob pattern
|
|
||||||
python main.py "path/to/images/*.zip"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Server Options
|
|
||||||
|
|
||||||
```bash
|
|
||||||
python main.py [SOURCE] [OPTIONS]
|
|
||||||
|
|
||||||
Arguments:
|
|
||||||
SOURCE Path to directory, ZIP archive, or glob pattern (e.g., *.zip, path/to/zips/*.zip)
|
|
||||||
|
|
||||||
Options:
|
|
||||||
--host TEXT Host to bind to (default: 0.0.0.0)
|
|
||||||
--port INTEGER Port to bind to (default: 8000)
|
|
||||||
--salt TEXT Salt for hashing file paths (default: random)
|
|
||||||
-h, --help Show help message and exit
|
|
||||||
```
|
|
||||||
|
|
||||||
### Examples
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Start server on default port 8000
|
|
||||||
python main.py ./photos
|
|
||||||
|
|
||||||
# Start server on custom host and port
|
|
||||||
python main.py ./photos --host 127.0.0.1 --port 3000
|
|
||||||
|
|
||||||
# Start server with custom salt for consistent hashing
|
|
||||||
python main.py ./photos --salt mysecretkey123
|
|
||||||
|
|
||||||
# Start server serving from ZIP files
|
|
||||||
python main.py ./archives/*.zip
|
|
||||||
```
|
|
||||||
|
|
||||||
## API Endpoints
|
|
||||||
|
|
||||||
### File Access
|
|
||||||
|
|
||||||
- `GET /api/{file_hash}/data` - Retrieve file data by hash
|
|
||||||
- Returns: File content with appropriate Content-Type header
|
|
||||||
- Response: StreamingResponse of the file data
|
|
||||||
|
|
||||||
### Navigation Pages
|
|
||||||
|
|
||||||
- `GET /{file_hash}` - View file with navigation controls
|
|
||||||
- Displays image with previous/next navigation
|
|
||||||
|
|
||||||
- `GET /{order}/{delay}/{file_hash}` - View file with auto-refresh
|
|
||||||
- order: "next" (sequential) or "random"
|
|
||||||
- delay: Seconds before auto-advancing
|
|
||||||
- file_hash: Starting file hash
|
|
||||||
|
|
||||||
### Special Endpoints
|
|
||||||
|
|
||||||
- `GET /` - Redirect to a random file
|
|
||||||
- `GET /{order}/{delay}` - Redirect to random file with order/delay settings
|
|
||||||
- `GET /api/health` - Health check endpoint
|
|
||||||
- Returns: JSON with status and file count
|
|
||||||
|
|
||||||
## How It Works
|
|
||||||
|
|
||||||
### File Indexing
|
|
||||||
|
|
||||||
When started, the server indexes all files from the provided source(s):
|
|
||||||
- For directories: Walks the directory tree and indexes all files
|
|
||||||
- For ZIP archives: Indexes all files within the archive
|
|
||||||
- For glob patterns: Processes each matching file
|
|
||||||
|
|
||||||
Each file is assigned a unique SHA-256 hash based on:
|
|
||||||
- File path (relative to source or within ZIP)
|
|
||||||
- Salt value (random or user-provided)
|
|
||||||
|
|
||||||
### Security
|
|
||||||
|
|
||||||
- Direct file path access is prevented
|
|
||||||
- Files can only be accessed via their cryptographic hashes
|
|
||||||
- Salt prevents hash prediction attacks
|
|
||||||
- No filesystem traversal vulnerabilities
|
|
||||||
|
|
||||||
### User Interface
|
|
||||||
|
|
||||||
The server serves a single-page application (`frontend.html`) that provides:
|
|
||||||
- Full-screen image display
|
|
||||||
- Click-to-navigate on image
|
|
||||||
- Chevron buttons for previous/next navigation
|
|
||||||
- Play/pause button for auto-advance control
|
|
||||||
- Responsive design that works on mobile and desktop
|
|
||||||
|
|
||||||
## Development
|
|
||||||
|
|
||||||
### Running in Development Mode
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# With auto-reload for development
|
|
||||||
uvicorn main:app --reload --host 0.0.0.0 --port 8000
|
|
||||||
```
|
|
||||||
|
|
||||||
### Code Structure
|
|
||||||
|
|
||||||
- `main.py` - Contains all server logic, routing, and file indexing
|
|
||||||
- `frontend.html` - HTML template for the user interface
|
|
||||||
- `FileIndexer` - Base class for indexing files from directories
|
|
||||||
- `ZipFileIndexer` - Extension for indexing files from ZIP archives
|
|
||||||
- Global `file_mapping` dictionary - Maps hashes to file paths/filenames
|
|
||||||
|
|
||||||
### Adding New Features
|
|
||||||
|
|
||||||
1. **New Indexer Types**: Create a class inheriting from `FileIndexer` and override the `_index()` method
|
|
||||||
2. **New Routes**: Add functions decorated with appropriate HTTP method decorators (`@app.get`, etc.)
|
|
||||||
3. **UI Changes**: Modify `frontend.html` template and update template substitution in `_render_page()`
|
|
||||||
|
|
||||||
## Dependencies
|
|
||||||
|
|
||||||
- **FastAPI** - Modern web framework for building APIs
|
|
||||||
- **Uvicorn** - ASGI server for running the application
|
|
||||||
- **Python Standard Library** - hashlib, mimetypes, secrets, zipfile, etc.
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
This project is licensed under the MIT License - see the LICENSE file for details.
|
|
||||||
|
|
||||||
## Contributing
|
|
||||||
|
|
||||||
1. Fork the repository
|
|
||||||
2. Create a feature branch
|
|
||||||
3. Commit your changes
|
|
||||||
4. Push to the branch
|
|
||||||
5. Open a pull request
|
|
||||||
|
|
||||||
Please ensure your code follows the existing style and includes appropriate tests.
|
|
||||||
@ -81,14 +81,14 @@
|
|||||||
if (e.code === 'Space') {
|
if (e.code === 'Space') {
|
||||||
var playBtn = document.querySelector('.play-btn');
|
var playBtn = document.querySelector('.play-btn');
|
||||||
if (playBtn) playBtn.click();
|
if (playBtn) playBtn.click();
|
||||||
} else if (e.code === 'ArrowLeft' || e.key.toLowerCase() === 'h') {
|
} else if (e.code === 'ArrowLeft') {
|
||||||
document.getElementById('prev-btn').click();
|
document.getElementById('prev-btn').click();
|
||||||
} else if (e.code === 'ArrowRight' || e.key.toLowerCase() === 'l') {
|
} else if (e.code === 'ArrowRight') {
|
||||||
document.getElementById('next-btn').click();
|
document.getElementById('next-btn').click();
|
||||||
} else if (e.code === 'Equal' || e.key.toLowerCase() === 'j') {
|
} else if (e.code === 'Equal') {
|
||||||
var params = getRefreshParams();
|
var params = getRefreshParams();
|
||||||
if (params) window.location.href = '/' + params.order + '/' + (params.delay + 1) + '/' + params.hash;
|
if (params) window.location.href = '/' + params.order + '/' + (params.delay + 1) + '/' + params.hash;
|
||||||
} else if (e.code === 'Minus' || e.key.toLowerCase() === 'k') {
|
} else if (e.code === 'Minus') {
|
||||||
var params = getRefreshParams();
|
var params = getRefreshParams();
|
||||||
if (params && params.delay > 1) window.location.href = '/' + params.order + '/' + (params.delay - 1) + '/' + params.hash;
|
if (params && params.delay > 1) window.location.href = '/' + params.order + '/' + (params.delay - 1) + '/' + params.hash;
|
||||||
} else if (e.key.toLowerCase() === 'o') {
|
} else if (e.key.toLowerCase() === 'o') {
|
||||||
|
|||||||
123
main.py
123
main.py
@ -1,4 +1,3 @@
|
|||||||
from typing import Annotated
|
|
||||||
import argparse
|
import argparse
|
||||||
import hashlib
|
import hashlib
|
||||||
import mimetypes
|
import mimetypes
|
||||||
@ -14,12 +13,7 @@ from pathlib import Path
|
|||||||
|
|
||||||
from fastapi import FastAPI, HTTPException, Depends
|
from fastapi import FastAPI, HTTPException, Depends
|
||||||
from fastapi.security import HTTPBasic, HTTPBasicCredentials
|
from fastapi.security import HTTPBasic, HTTPBasicCredentials
|
||||||
from fastapi.responses import (
|
from fastapi.responses import FileResponse, HTMLResponse, RedirectResponse, StreamingResponse
|
||||||
FileResponse,
|
|
||||||
HTMLResponse,
|
|
||||||
RedirectResponse,
|
|
||||||
StreamingResponse,
|
|
||||||
)
|
|
||||||
|
|
||||||
app = FastAPI()
|
app = FastAPI()
|
||||||
file_mapping = {}
|
file_mapping = {}
|
||||||
@ -29,9 +23,7 @@ security = HTTPBasic()
|
|||||||
expected_password: str | None = None
|
expected_password: str | None = None
|
||||||
|
|
||||||
|
|
||||||
async def get_current_username(
|
async def get_current_username(credentials: Annotated[HTTPBasicCredentials, Depends(security)]) -> str:
|
||||||
credentials: Annotated[HTTPBasicCredentials, Depends(security)],
|
|
||||||
) -> str:
|
|
||||||
"""Verify Basic Authentication credentials"""
|
"""Verify Basic Authentication credentials"""
|
||||||
if expected_password is not None and credentials.password != expected_password:
|
if expected_password is not None and credentials.password != expected_password:
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
@ -189,9 +181,7 @@ async def root(username: str = Depends(get_current_username)):
|
|||||||
|
|
||||||
|
|
||||||
@app.get("/{order}/{delay}")
|
@app.get("/{order}/{delay}")
|
||||||
async def order_delay(
|
async def order_delay(order: str, delay: int, username: str = Depends(get_current_username)):
|
||||||
order: str, delay: int, username: str = Depends(get_current_username)
|
|
||||||
):
|
|
||||||
"""Redirect to random file with order and delay"""
|
"""Redirect to random file with order and delay"""
|
||||||
random_hash = _get_random_hash()
|
random_hash = _get_random_hash()
|
||||||
return RedirectResponse(
|
return RedirectResponse(
|
||||||
@ -199,27 +189,13 @@ async def order_delay(
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def _get_navigation_data(file_hash: str, order: str | None = None):
|
def _get_navigation_data(file_hash: str):
|
||||||
"""Get navigation data for a file hash.
|
"""Get navigation data for a file hash"""
|
||||||
|
|
||||||
Args:
|
|
||||||
file_hash: The current file's hash.
|
|
||||||
order: Navigation order - 'next' for sequential, 'random' for random,
|
|
||||||
or None for default browse mode.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Dictionary with navigation hashes and filename.
|
|
||||||
"""
|
|
||||||
keys = list(file_mapping.keys())
|
keys = list(file_mapping.keys())
|
||||||
idx = keys.index(file_hash)
|
idx = keys.index(file_hash)
|
||||||
|
next_hash = keys[(idx + 1) % len(keys)]
|
||||||
if order == "random":
|
prev_hash = keys[idx - 1] if idx > 0 else keys[-1]
|
||||||
next_hash = _get_random_hash()
|
next_random_hash = _get_random_hash()
|
||||||
prev_hash = _get_random_hash()
|
|
||||||
else:
|
|
||||||
next_hash = keys[(idx + 1) % len(keys)]
|
|
||||||
prev_hash = keys[idx - 1] if idx > 0 else keys[-1]
|
|
||||||
|
|
||||||
indexer = _find_indexer_for_hash(file_hash)
|
indexer = _find_indexer_for_hash(file_hash)
|
||||||
filename = indexer.get_filename_by_hash(file_hash) if indexer else ""
|
filename = indexer.get_filename_by_hash(file_hash) if indexer else ""
|
||||||
|
|
||||||
@ -227,47 +203,25 @@ def _get_navigation_data(file_hash: str, order: str | None = None):
|
|||||||
"file_hash": file_hash,
|
"file_hash": file_hash,
|
||||||
"next_hash": next_hash,
|
"next_hash": next_hash,
|
||||||
"prev_hash": prev_hash,
|
"prev_hash": prev_hash,
|
||||||
|
"next_random_hash": next_random_hash,
|
||||||
"filename": filename,
|
"filename": filename,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
def _render_page(
|
def _render_page(
|
||||||
navigation_data: dict,
|
navigation_data: dict, extra_meta: str = "", image_click_url: str = "", play_button: str = ""
|
||||||
extra_meta: str = "",
|
|
||||||
image_click_url: str = "",
|
|
||||||
play_button: str = "",
|
|
||||||
current_order: str | None = None,
|
|
||||||
current_delay: int | None = None,
|
|
||||||
) -> HTMLResponse:
|
) -> HTMLResponse:
|
||||||
"""Render the frontend page with navigation data"""
|
"""Render the frontend page with navigation data"""
|
||||||
with open("frontend.html", "r") as f:
|
with open("frontend.html", "r") as f:
|
||||||
content = f.read()
|
content = f.read()
|
||||||
|
|
||||||
template = string.Template(content)
|
template = string.Template(content)
|
||||||
|
|
||||||
# Generate navigation URLs based on current mode
|
|
||||||
if current_order is not None:
|
|
||||||
# Timer mode: preserve current order and delay
|
|
||||||
next_url = "/{order}/{delay}/{next_hash}".format(
|
|
||||||
order=current_order,
|
|
||||||
delay=current_delay,
|
|
||||||
next_hash=navigation_data["next_hash"],
|
|
||||||
)
|
|
||||||
prev_url = "/{order}/{delay}/{prev_hash}".format(
|
|
||||||
order=current_order,
|
|
||||||
delay=current_delay,
|
|
||||||
prev_hash=navigation_data["prev_hash"],
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
# Browse mode: generate browse mode URLs
|
|
||||||
next_url = "/{next_hash}".format(next_hash=navigation_data["next_hash"])
|
|
||||||
prev_url = "/{prev_hash}".format(prev_hash=navigation_data["prev_hash"])
|
|
||||||
|
|
||||||
content = template.substitute(
|
content = template.substitute(
|
||||||
img_url="/api/{file_hash}/data".format(file_hash=navigation_data["file_hash"]),
|
img_url="/api/{file_hash}/data".format(file_hash=navigation_data["file_hash"]),
|
||||||
image_click_url=image_click_url or _get_random_hash(),
|
image_click_url=image_click_url
|
||||||
next_url=next_url,
|
or "/{next_random_hash}".format(next_random_hash=navigation_data["next_random_hash"]),
|
||||||
prev_url=prev_url,
|
next_url="/{next_hash}".format(next_hash=navigation_data["next_hash"]),
|
||||||
|
prev_url="/{prev_hash}".format(prev_hash=navigation_data["prev_hash"]),
|
||||||
filename=navigation_data["filename"],
|
filename=navigation_data["filename"],
|
||||||
extra_meta=extra_meta,
|
extra_meta=extra_meta,
|
||||||
play_button=play_button,
|
play_button=play_button,
|
||||||
@ -282,36 +236,32 @@ async def hash_page(file_hash: str, username: str = Depends(get_current_username
|
|||||||
if file_hash not in file_mapping:
|
if file_hash not in file_mapping:
|
||||||
raise HTTPException(status_code=404, detail="File not found")
|
raise HTTPException(status_code=404, detail="File not found")
|
||||||
|
|
||||||
navigation_data = _get_navigation_data(file_hash, order=None)
|
navigation_data = _get_navigation_data(file_hash)
|
||||||
play_button = '<a href="/next/5/{file_hash}" class="play-btn" title="Play next 5">⏵</a>'.format(
|
play_button = '<a href="/next/5/{file_hash}" class="play-btn" title="Play next 5">⏵</a>'.format(
|
||||||
file_hash=file_hash
|
file_hash=file_hash
|
||||||
)
|
)
|
||||||
return _render_page(
|
return _render_page(navigation_data, play_button=play_button)
|
||||||
navigation_data, play_button=play_button, current_order=None, current_delay=None
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@app.get("/{order}/{delay}/{file_hash}")
|
@app.get("/{order}/{delay}/{file_hash}")
|
||||||
async def hash_page_with_refresh(
|
async def hash_page_with_refresh(order: str, delay: int, file_hash: str, username: str = Depends(get_current_username)):
|
||||||
order: str,
|
|
||||||
delay: int,
|
|
||||||
file_hash: str,
|
|
||||||
username: str = Depends(get_current_username),
|
|
||||||
):
|
|
||||||
"""Serve a page for a specific file hash with auto-refresh navigation"""
|
"""Serve a page for a specific file hash with auto-refresh navigation"""
|
||||||
if file_hash not in file_mapping:
|
if file_hash not in file_mapping:
|
||||||
raise HTTPException(status_code=404, detail="File not found")
|
raise HTTPException(status_code=404, detail="File not found")
|
||||||
|
|
||||||
if order not in ("next", "random"):
|
if order not in ("next", "random"):
|
||||||
raise HTTPException(
|
raise HTTPException(status_code=400, detail="Invalid order. Must be 'next' or 'random'")
|
||||||
status_code=400, detail="Invalid order. Must be 'next' or 'random'"
|
|
||||||
|
navigation_data = _get_navigation_data(file_hash)
|
||||||
|
|
||||||
|
if order == "next":
|
||||||
|
refresh_url = "/{order}/{delay}/{next_hash}".format(
|
||||||
|
order=order, delay=delay, next_hash=navigation_data["next_hash"]
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
refresh_url = "/{order}/{delay}/{next_random_hash}".format(
|
||||||
|
order=order, delay=delay, next_random_hash=navigation_data["next_random_hash"]
|
||||||
)
|
)
|
||||||
|
|
||||||
navigation_data = _get_navigation_data(file_hash, order=order)
|
|
||||||
|
|
||||||
refresh_url = "/{order}/{delay}/{next_hash}".format(
|
|
||||||
order=order, delay=delay, next_hash=navigation_data["next_hash"]
|
|
||||||
)
|
|
||||||
|
|
||||||
refresh_meta = f'<meta http-equiv="refresh" content="{delay};url={refresh_url}">'
|
refresh_meta = f'<meta http-equiv="refresh" content="{delay};url={refresh_url}">'
|
||||||
image_click_url = "/{file_hash}".format(file_hash=file_hash)
|
image_click_url = "/{file_hash}".format(file_hash=file_hash)
|
||||||
@ -321,14 +271,7 @@ async def hash_page_with_refresh(
|
|||||||
file_hash=file_hash
|
file_hash=file_hash
|
||||||
)
|
)
|
||||||
|
|
||||||
return _render_page(
|
return _render_page(navigation_data, refresh_meta, image_click_url, play_button=pause_button)
|
||||||
navigation_data,
|
|
||||||
refresh_meta,
|
|
||||||
image_click_url,
|
|
||||||
play_button=pause_button,
|
|
||||||
current_order=order,
|
|
||||||
current_delay=delay,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def _find_indexer_for_hash(file_hash: str):
|
def _find_indexer_for_hash(file_hash: str):
|
||||||
@ -356,12 +299,8 @@ if __name__ == "__main__":
|
|||||||
)
|
)
|
||||||
parser.add_argument("--host", type=str, default="0.0.0.0", help="Host to bind to")
|
parser.add_argument("--host", type=str, default="0.0.0.0", help="Host to bind to")
|
||||||
parser.add_argument("--port", type=int, default=8000, help="Port to bind to")
|
parser.add_argument("--port", type=int, default=8000, help="Port to bind to")
|
||||||
parser.add_argument(
|
parser.add_argument("--salt", type=str, default=None, help="Salt for hashing file paths")
|
||||||
"--salt", type=str, default=None, help="Salt for hashing file paths"
|
parser.add_argument("--password", type=str, default=None, help="Password for Basic Authentication")
|
||||||
)
|
|
||||||
parser.add_argument(
|
|
||||||
"--password", type=str, default=None, help="Password for Basic Authentication"
|
|
||||||
)
|
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
initialize_server(args)
|
initialize_server(args)
|
||||||
|
|||||||
@ -5,7 +5,6 @@ description = "Add your description here"
|
|||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.13"
|
requires-python = ">=3.13"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"black>=26.3.1",
|
|
||||||
"fastapi>=0.128.0",
|
"fastapi>=0.128.0",
|
||||||
"uvicorn>=0.40.0",
|
"uvicorn>=0.40.0",
|
||||||
]
|
]
|
||||||
|
|||||||
89
uv.lock
generated
89
uv.lock
generated
@ -32,33 +32,6 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" },
|
{ url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "black"
|
|
||||||
version = "26.3.1"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
dependencies = [
|
|
||||||
{ name = "click" },
|
|
||||||
{ name = "mypy-extensions" },
|
|
||||||
{ name = "packaging" },
|
|
||||||
{ name = "pathspec" },
|
|
||||||
{ name = "platformdirs" },
|
|
||||||
{ name = "pytokens" },
|
|
||||||
]
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/e1/c5/61175d618685d42b005847464b8fb4743a67b1b8fdb75e50e5a96c31a27a/black-26.3.1.tar.gz", hash = "sha256:2c50f5063a9641c7eed7795014ba37b0f5fa227f3d408b968936e24bc0566b07", size = 666155, upload-time = "2026-03-12T03:36:03.593Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/f5/77/5728052a3c0450c53d9bb3945c4c46b91baa62b2cafab6801411b6271e45/black-26.3.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:855822d90f884905362f602880ed8b5df1b7e3ee7d0db2502d4388a954cc8c54", size = 1895034, upload-time = "2026-03-12T03:40:21.813Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/52/73/7cae55fdfdfbe9d19e9a8d25d145018965fe2079fa908101c3733b0c55a0/black-26.3.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8a33d657f3276328ce00e4d37fe70361e1ec7614da5d7b6e78de5426cb56332f", size = 1718503, upload-time = "2026-03-12T03:40:23.666Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/e1/87/af89ad449e8254fdbc74654e6467e3c9381b61472cc532ee350d28cfdafb/black-26.3.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f1cd08e99d2f9317292a311dfe578fd2a24b15dbce97792f9c4d752275c1fa56", size = 1793557, upload-time = "2026-03-12T03:40:25.497Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/43/10/d6c06a791d8124b843bf325ab4ac7d2f5b98731dff84d6064eafd687ded1/black-26.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:c7e72339f841b5a237ff14f7d3880ddd0fc7f98a1199e8c4327f9a4f478c1839", size = 1422766, upload-time = "2026-03-12T03:40:27.14Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/59/4f/40a582c015f2d841ac24fed6390bd68f0fc896069ff3a886317959c9daf8/black-26.3.1-cp313-cp313-win_arm64.whl", hash = "sha256:afc622538b430aa4c8c853f7f63bc582b3b8030fd8c80b70fb5fa5b834e575c2", size = 1232140, upload-time = "2026-03-12T03:40:28.882Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d5/da/e36e27c9cebc1311b7579210df6f1c86e50f2d7143ae4fcf8a5017dc8809/black-26.3.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2d6bfaf7fd0993b420bed691f20f9492d53ce9a2bcccea4b797d34e947318a78", size = 1889234, upload-time = "2026-03-12T03:40:30.964Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/0e/7b/9871acf393f64a5fa33668c19350ca87177b181f44bb3d0c33b2d534f22c/black-26.3.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f89f2ab047c76a9c03f78d0d66ca519e389519902fa27e7a91117ef7611c0568", size = 1720522, upload-time = "2026-03-12T03:40:32.346Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/03/87/e766c7f2e90c07fb7586cc787c9ae6462b1eedab390191f2b7fc7f6170a9/black-26.3.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b07fc0dab849d24a80a29cfab8d8a19187d1c4685d8a5e6385a5ce323c1f015f", size = 1787824, upload-time = "2026-03-12T03:40:33.636Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/ac/94/2424338fb2d1875e9e83eed4c8e9c67f6905ec25afd826a911aea2b02535/black-26.3.1-cp314-cp314-win_amd64.whl", hash = "sha256:0126ae5b7c09957da2bdbd91a9ba1207453feada9e9fe51992848658c6c8e01c", size = 1445855, upload-time = "2026-03-12T03:40:35.442Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/86/43/0c3338bd928afb8ee7471f1a4eec3bdbe2245ccb4a646092a222e8669840/black-26.3.1-cp314-cp314-win_arm64.whl", hash = "sha256:92c0ec1f2cc149551a2b7b47efc32c866406b6891b0ee4625e95967c8f4acfb1", size = 1258109, upload-time = "2026-03-12T03:40:36.832Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/8e/0d/52d98722666d6fc6c3dd4c76df339501d6efd40e0ff95e6186a7b7f0befd/black-26.3.1-py3-none-any.whl", hash = "sha256:2bd5aa94fc267d38bb21a70d7410a89f1a1d318841855f698746f8e7f51acd1b", size = 207542, upload-time = "2026-03-12T03:36:01.668Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "click"
|
name = "click"
|
||||||
version = "8.3.1"
|
version = "8.3.1"
|
||||||
@ -100,7 +73,6 @@ name = "file-server"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
source = { virtual = "." }
|
source = { virtual = "." }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "black" },
|
|
||||||
{ name = "fastapi" },
|
{ name = "fastapi" },
|
||||||
{ name = "uvicorn" },
|
{ name = "uvicorn" },
|
||||||
]
|
]
|
||||||
@ -117,7 +89,6 @@ dev = [
|
|||||||
|
|
||||||
[package.metadata]
|
[package.metadata]
|
||||||
requires-dist = [
|
requires-dist = [
|
||||||
{ name = "black", specifier = ">=26.3.1" },
|
|
||||||
{ name = "fastapi", specifier = ">=0.128.0" },
|
{ name = "fastapi", specifier = ">=0.128.0" },
|
||||||
{ name = "ruff", marker = "extra == 'dev'", specifier = ">=0.15.5" },
|
{ name = "ruff", marker = "extra == 'dev'", specifier = ">=0.15.5" },
|
||||||
{ name = "uvicorn", specifier = ">=0.40.0" },
|
{ name = "uvicorn", specifier = ">=0.40.0" },
|
||||||
@ -145,42 +116,6 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" },
|
{ url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "mypy-extensions"
|
|
||||||
version = "1.1.0"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "packaging"
|
|
||||||
version = "26.1"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/df/de/0d2b39fb4af88a0258f3bac87dfcbb48e73fbdea4a2ed0e2213f9a4c2f9a/packaging-26.1.tar.gz", hash = "sha256:f042152b681c4bfac5cae2742a55e103d27ab2ec0f3d88037136b6bfe7c9c5de", size = 215519, upload-time = "2026-04-14T21:12:49.362Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/7a/c2/920ef838e2f0028c8262f16101ec09ebd5969864e5a64c4c05fad0617c56/packaging-26.1-py3-none-any.whl", hash = "sha256:5d9c0669c6285e491e0ced2eee587eaf67b670d94a19e94e3984a481aba6802f", size = 95831, upload-time = "2026-04-14T21:12:47.56Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pathspec"
|
|
||||||
version = "1.1.0"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/2e/17/9c3094b822982b9f1ea666d8580ce59000f61f87c1663556fb72031ad9ec/pathspec-1.1.0.tar.gz", hash = "sha256:f5d7c555da02fd8dde3e4a2354b6aba817a89112fa8f333f7917a2a4834dd080", size = 133918, upload-time = "2026-04-23T01:46:22.298Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/fa/c9/8eed0486f074e9f1ca7f8ce5ad663e65f12fdab344028d658fa1b03d35e0/pathspec-1.1.0-py3-none-any.whl", hash = "sha256:574b128f7456bd899045ccd142dd446af7e6cfd0072d63ad73fbc55fbb4aaa42", size = 56264, upload-time = "2026-04-23T01:46:20.606Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "platformdirs"
|
|
||||||
version = "4.9.6"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/9f/4a/0883b8e3802965322523f0b200ecf33d31f10991d0401162f4b23c698b42/platformdirs-4.9.6.tar.gz", hash = "sha256:3bfa75b0ad0db84096ae777218481852c0ebc6c727b3168c1b9e0118e458cf0a", size = 29400, upload-time = "2026-04-09T00:04:10.812Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/75/a6/a0a304dc33b49145b21f4808d763822111e67d1c3a32b524a1baf947b6e1/platformdirs-4.9.6-py3-none-any.whl", hash = "sha256:e61adb1d5e5cb3441b4b7710bea7e4c12250ca49439228cc1021c00dcfac0917", size = 21348, upload-time = "2026-04-09T00:04:09.463Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pydantic"
|
name = "pydantic"
|
||||||
version = "2.12.5"
|
version = "2.12.5"
|
||||||
@ -249,30 +184,6 @@ wheels = [
|
|||||||
{ url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" },
|
{ url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pytokens"
|
|
||||||
version = "0.4.1"
|
|
||||||
source = { registry = "https://pypi.org/simple" }
|
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/b6/34/b4e015b99031667a7b960f888889c5bd34ef585c85e1cb56a594b92836ac/pytokens-0.4.1.tar.gz", hash = "sha256:292052fe80923aae2260c073f822ceba21f3872ced9a68bb7953b348e561179a", size = 23015, upload-time = "2026-01-30T01:03:45.924Z" }
|
|
||||||
wheels = [
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/cb/dc/08b1a080372afda3cceb4f3c0a7ba2bde9d6a5241f1edb02a22a019ee147/pytokens-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8bdb9d0ce90cbf99c525e75a2fa415144fd570a1ba987380190e8b786bc6ef9b", size = 160720, upload-time = "2026-01-30T01:03:13.843Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/64/0c/41ea22205da480837a700e395507e6a24425151dfb7ead73343d6e2d7ffe/pytokens-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5502408cab1cb18e128570f8d598981c68a50d0cbd7c61312a90507cd3a1276f", size = 254204, upload-time = "2026-01-30T01:03:14.886Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/e0/d2/afe5c7f8607018beb99971489dbb846508f1b8f351fcefc225fcf4b2adc0/pytokens-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29d1d8fb1030af4d231789959f21821ab6325e463f0503a61d204343c9b355d1", size = 268423, upload-time = "2026-01-30T01:03:15.936Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/68/d4/00ffdbd370410c04e9591da9220a68dc1693ef7499173eb3e30d06e05ed1/pytokens-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:970b08dd6b86058b6dc07efe9e98414f5102974716232d10f32ff39701e841c4", size = 266859, upload-time = "2026-01-30T01:03:17.458Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/a7/c9/c3161313b4ca0c601eeefabd3d3b576edaa9afdefd32da97210700e47652/pytokens-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:9bd7d7f544d362576be74f9d5901a22f317efc20046efe2034dced238cbbfe78", size = 103520, upload-time = "2026-01-30T01:03:18.652Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/8f/a7/b470f672e6fc5fee0a01d9e75005a0e617e162381974213a945fcd274843/pytokens-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4a14d5f5fc78ce85e426aa159489e2d5961acf0e47575e08f35584009178e321", size = 160821, upload-time = "2026-01-30T01:03:19.684Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/80/98/e83a36fe8d170c911f864bfded690d2542bfcfacb9c649d11a9e6eb9dc41/pytokens-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97f50fd18543be72da51dd505e2ed20d2228c74e0464e4262e4899797803d7fa", size = 254263, upload-time = "2026-01-30T01:03:20.834Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/0f/95/70d7041273890f9f97a24234c00b746e8da86df462620194cef1d411ddeb/pytokens-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc74c035f9bfca0255c1af77ddd2d6ae8419012805453e4b0e7513e17904545d", size = 268071, upload-time = "2026-01-30T01:03:21.888Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/da/79/76e6d09ae19c99404656d7db9c35dfd20f2086f3eb6ecb496b5b31163bad/pytokens-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f66a6bbe741bd431f6d741e617e0f39ec7257ca1f89089593479347cc4d13324", size = 271716, upload-time = "2026-01-30T01:03:23.633Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/79/37/482e55fa1602e0a7ff012661d8c946bafdc05e480ea5a32f4f7e336d4aa9/pytokens-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:b35d7e5ad269804f6697727702da3c517bb8a5228afa450ab0fa787732055fc9", size = 104539, upload-time = "2026-01-30T01:03:24.788Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/30/e8/20e7db907c23f3d63b0be3b8a4fd1927f6da2395f5bcc7f72242bb963dfe/pytokens-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8fcb9ba3709ff77e77f1c7022ff11d13553f3c30299a9fe246a166903e9091eb", size = 168474, upload-time = "2026-01-30T01:03:26.428Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/d6/81/88a95ee9fafdd8f5f3452107748fd04c24930d500b9aba9738f3ade642cc/pytokens-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79fc6b8699564e1f9b521582c35435f1bd32dd06822322ec44afdeba666d8cb3", size = 290473, upload-time = "2026-01-30T01:03:27.415Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/cf/35/3aa899645e29b6375b4aed9f8d21df219e7c958c4c186b465e42ee0a06bf/pytokens-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d31b97b3de0f61571a124a00ffe9a81fb9939146c122c11060725bd5aea79975", size = 303485, upload-time = "2026-01-30T01:03:28.558Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/52/a0/07907b6ff512674d9b201859f7d212298c44933633c946703a20c25e9d81/pytokens-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:967cf6e3fd4adf7de8fc73cd3043754ae79c36475c1c11d514fc72cf5490094a", size = 306698, upload-time = "2026-01-30T01:03:29.653Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/39/2a/cbbf9250020a4a8dd53ba83a46c097b69e5eb49dd14e708f496f548c6612/pytokens-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:584c80c24b078eec1e227079d56dc22ff755e0ba8654d8383b2c549107528918", size = 116287, upload-time = "2026-01-30T01:03:30.912Z" },
|
|
||||||
{ url = "https://files.pythonhosted.org/packages/c6/78/397db326746f0a342855b81216ae1f0a32965deccfd7c830a2dbc66d2483/pytokens-0.4.1-py3-none-any.whl", hash = "sha256:26cef14744a8385f35d0e095dc8b3a7583f6c953c2e3d269c7f82484bf5ad2de", size = 13729, upload-time = "2026-01-30T01:03:45.029Z" },
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ruff"
|
name = "ruff"
|
||||||
version = "0.15.5"
|
version = "0.15.5"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user