Add prek + pytest-cov (#873)

This commit is contained in:
Alex
2026-04-12 12:39:15 +01:00
committed by GitHub
parent d7b9f2e67f
commit 87d5f127d6
26 changed files with 206 additions and 90 deletions
+4 -4
View File
@@ -11,9 +11,9 @@ assignees: ''
## Steps To Reproduce ## Steps To Reproduce
<!-- Steps to reproduce the behavior --> <!-- Steps to reproduce the behavior -->
1. 1.
2. 2.
3. 3.
## Expected Behavior ## Expected Behavior
<!-- A clear and concise description of what you expected to happen --> <!-- A clear and concise description of what you expected to happen -->
@@ -34,4 +34,4 @@ When running in debug mode, a DEBUG button will appear in the interface. Please
<!-- If applicable, please provide your full docker-compose (redacted from any secrets) --> <!-- If applicable, please provide your full docker-compose (redacted from any secrets) -->
## Additional Context ## Additional Context
<!-- Add any other context about the problem here --> <!-- Add any other context about the problem here -->
@@ -65,7 +65,7 @@ jobs:
- name: Get current date - name: Get current date
id: date id: date
run: echo "date=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT run: echo "date=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+13
View File
@@ -0,0 +1,13 @@
repos:
- repo: builtin
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-added-large-files
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.15.10
hooks:
- id: ruff-check
args: [--fix]
- id: ruff-format
+1 -1
View File
@@ -61,4 +61,4 @@
] ]
} }
] ]
} }
+11 -1
View File
@@ -1,4 +1,4 @@
.PHONY: help install install-python-dev dev build preview typecheck frontend-test clean up up down docker-build refresh restart build-serve python-lint python-lint-fix python-format python-format-check python-typecheck python-dead-code python-checks python-test-lint python-test-lint-fix python-test-format python-test-format-check python-test-typecheck python-test-checks .PHONY: help install install-python-dev dev build preview typecheck frontend-test clean up up down docker-build refresh restart build-serve python-lint python-lint-fix python-format python-format-check python-typecheck python-dead-code python-checks python-test-lint python-test-lint-fix python-test-format python-test-format-check python-test-typecheck python-test-checks python-coverage prek-install
# Frontend directory # Frontend directory
FRONTEND_DIR := src/frontend FRONTEND_DIR := src/frontend
@@ -32,6 +32,8 @@ help:
@echo " python-test-format-check - Check Python test formatting with Ruff" @echo " python-test-format-check - Check Python test formatting with Ruff"
@echo " python-test-typecheck - Run lightweight BasedPyright checks against Python tests" @echo " python-test-typecheck - Run lightweight BasedPyright checks against Python tests"
@echo " python-test-checks - Run all relaxed Python test static analysis checks" @echo " python-test-checks - Run all relaxed Python test static analysis checks"
@echo " python-coverage - Run tests with coverage report"
@echo " prek-install - Install prek git hooks"
@echo " clean - Remove node_modules and build artifacts" @echo " clean - Remove node_modules and build artifacts"
@echo "" @echo ""
@echo "Backend (Docker):" @echo "Backend (Docker):"
@@ -127,6 +129,14 @@ python-test-typecheck:
python-test-checks: python-test-lint python-test-format-check python-test-typecheck python-test-checks: python-test-lint python-test-format-check python-test-typecheck
python-coverage:
@echo "Running tests with coverage..."
uv run pytest tests/ -x --tb=short -m "not integration and not e2e" --cov --cov-report=term-missing
prek-install:
@echo "Installing prek git hooks..."
uv run prek install
# Run frontend unit tests # Run frontend unit tests
frontend-test: frontend-test:
@echo "Running frontend unit tests..." @echo "Running frontend unit tests..."
+1 -1
View File
@@ -69,4 +69,4 @@
{ "language": "Uyghur", "code": "ug" }, { "language": "Uyghur", "code": "ug" },
{ "language": "Armenian", "code": "hy" }, { "language": "Armenian", "code": "hy" },
{ "language": "Shan", "code": "shn" } { "language": "Shan", "code": "shn" }
] ]
+1 -1
View File
@@ -331,7 +331,7 @@ Directory where downloaded files are saved. Use {User} for per-user folders (e.g
**File Organization** **File Organization**
Choose how downloaded book files are named and organized. Choose how downloaded book files are named and organized.
- **Type:** string (choice) - **Type:** string (choice)
- **Default:** `rename` - **Default:** `rename`
+1 -1
View File
@@ -456,7 +456,7 @@ if [ "$DEBUG" = "true" ] && [ "$USING_EXTERNAL_BYPASSER" != "true" ]; then
--enable-logging --v=1 --log-level=0 \ --enable-logging --v=1 --log-level=0 \
--log-file=/tmp/chrome_entrypoint_test.log \ --log-file=/tmp/chrome_entrypoint_test.log \
--crash-dumps-dir=/tmp/chrome_crash_dumps \ --crash-dumps-dir=/tmp/chrome_crash_dumps \
< /dev/null < /dev/null
EXIT_CODE=$? EXIT_CODE=$?
echo "Chrome exit code: $EXIT_CODE" echo "Chrome exit code: $EXIT_CODE"
ls -lh /tmp/chrome_entrypoint_test.log ls -lh /tmp/chrome_entrypoint_test.log
-1
View File
@@ -199,4 +199,3 @@ else
echo "Failed to create debug archive" echo "Failed to create debug archive"
exit 1 exit 1
fi fi
+15
View File
@@ -36,7 +36,9 @@ browser = [
[dependency-groups] [dependency-groups]
dev = [ dev = [
"basedpyright>=1.39.0", "basedpyright>=1.39.0",
"prek",
"pytest", "pytest",
"pytest-cov",
"pytest-xdist>=3.8.0", "pytest-xdist>=3.8.0",
"ruff==0.15.10", "ruff==0.15.10",
"vulture>=2.14", "vulture>=2.14",
@@ -93,6 +95,11 @@ select = [
ignore = ["D", "EM", "FBT", "PLR2004", "UP035", "TRY003", "E501", "TD002", "S104", "S603"] ignore = ["D", "EM", "FBT", "PLR2004", "UP035", "TRY003", "E501", "TD002", "S104", "S603"]
[tool.ruff.lint.per-file-ignores] [tool.ruff.lint.per-file-ignores]
"scripts/**/*.py" = [
"BLE001",
"S",
"TRY",
]
"tests/**/*.py" = [ "tests/**/*.py" = [
"ANN", "ANN",
"BLE001", "BLE001",
@@ -153,5 +160,13 @@ ignore_decorators = [
min_confidence = 90 min_confidence = 90
sort_by_size = true sort_by_size = true
[tool.coverage.run]
source = ["shelfmark"]
branch = true
[tool.coverage.report]
show_missing = true
skip_empty = true
[tool.uv] [tool.uv]
package = false package = false
+1 -1
View File
@@ -68,7 +68,7 @@ That's it! Configure settings through the web interface as needed.
volumes: volumes:
- /your/config/path:/config # Config, database, and artwork cache directory - /your/config/path:/config # Config, database, and artwork cache directory
- /your/download/path:/books # Downloaded books - /your/download/path:/books # Downloaded books
- /client/path:/client/path # Optional: For Torrent/Usenet downloads, match your client directory exactly. - /client/path:/client/path # Optional: For Torrent/Usenet downloads, match your client directory exactly.
``` ```
> **Tip**: Point the download volume to your CWA or Grimmory ingest folder for automatic import. > **Tip**: Point the download volume to your CWA or Grimmory ingest folder for automatic import.
+32 -36
View File
@@ -17,16 +17,15 @@ The generated documentation includes:
import argparse import argparse
import sys import sys
from datetime import datetime
from pathlib import Path from pathlib import Path
from typing import Any, Dict, List, Optional from typing import Any
# Add project root to path # Add project root to path
project_root = Path(__file__).resolve().parent.parent project_root = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(project_root)) sys.path.insert(0, str(project_root))
def get_field_type_name(field) -> str: def get_field_type_name(field: Any) -> str:
"""Get a human-readable type name for a field.""" """Get a human-readable type name for a field."""
from shelfmark.core.settings_registry import ( from shelfmark.core.settings_registry import (
CheckboxField, CheckboxField,
@@ -40,49 +39,47 @@ def get_field_type_name(field) -> str:
if isinstance(field, CheckboxField): if isinstance(field, CheckboxField):
return "boolean" return "boolean"
elif isinstance(field, NumberField): if isinstance(field, NumberField):
return "number" return "number"
elif isinstance(field, SelectField): if isinstance(field, SelectField):
return "string (choice)" return "string (choice)"
elif isinstance(field, MultiSelectField): if isinstance(field, MultiSelectField):
return "string (comma-separated)" return "string (comma-separated)"
elif isinstance(field, OrderableListField): if isinstance(field, OrderableListField):
return "JSON array" return "JSON array"
elif isinstance(field, PasswordField): if isinstance(field, PasswordField):
return "string (secret)" return "string (secret)"
elif isinstance(field, TextField): if isinstance(field, TextField):
return "string"
else:
return "string" return "string"
return "string"
def format_default_value(field) -> str: def format_default_value(field: Any) -> str:
"""Format the default value for display.""" """Format the default value for display."""
default = field.default default = field.default
if default is None: if default is None:
return "_none_" return "_none_"
elif isinstance(default, bool): if isinstance(default, bool):
return f"`{str(default).lower()}`" return f"`{str(default).lower()}`"
elif isinstance(default, (int, float)): if isinstance(default, (int, float)):
return f"`{default}`" return f"`{default}`"
elif isinstance(default, str): if isinstance(default, str):
if default == "": if default == "":
return "_empty string_" return "_empty string_"
return f"`{default}`" return f"`{default}`"
elif isinstance(default, list): if isinstance(default, list):
if not default: if not default:
return "_empty list_" return "_empty list_"
# For simple lists, show comma-separated values # For simple lists, show comma-separated values
if all(isinstance(item, str) for item in default): if all(isinstance(item, str) for item in default):
return f"`{','.join(default)}`" return f"`{','.join(default)}`"
# For complex lists (e.g., OrderableListField defaults), summarize # For complex lists (e.g., OrderableListField defaults), summarize
return f"_see UI for defaults_" return "_see UI for defaults_"
else: return f"`{default}`"
return f"`{default}`"
def get_select_options(field) -> Optional[List[str]]: def get_select_options(field: Any) -> list[str] | None:
"""Get the available options for a SelectField. """Get the available options for a SelectField.
Returns options formatted as 'value (label)' or just 'value' if they match, Returns options formatted as 'value (label)' or just 'value' if they match,
@@ -119,7 +116,7 @@ def get_select_options(field) -> Optional[List[str]]:
return result return result
def _generate_bootstrap_env_docs() -> List[str]: def _generate_bootstrap_env_docs() -> list[str]:
"""Generate documentation for bootstrap environment variables from env.py.""" """Generate documentation for bootstrap environment variables from env.py."""
# These are environment variables defined in env.py that are used before # These are environment variables defined in env.py that are used before
# the settings registry is available # the settings registry is available
@@ -195,8 +192,10 @@ def _generate_bootstrap_env_docs() -> List[str]:
"|----------|-------------|------|---------|", "|----------|-------------|------|---------|",
] ]
for var in bootstrap_vars: lines.extend(
lines.append(f"| `{var['name']}` | {var['description']} | {var['type']} | `{var['default']}` |") f"| `{var['name']}` | {var['description']} | {var['type']} | `{var['default']}` |"
for var in bootstrap_vars
)
lines.append("") lines.append("")
lines.append("<details>") lines.append("<details>")
@@ -221,17 +220,14 @@ def _generate_bootstrap_env_docs() -> List[str]:
def generate_env_docs() -> str: def generate_env_docs() -> str:
"""Generate markdown documentation for all environment variables.""" """Generate markdown documentation for all environment variables."""
# Import settings modules to ensure all settings are registered # Import settings modules to ensure all settings are registered
import shelfmark.config.settings # noqa: F401 import shelfmark.config.security
import shelfmark.config.security # noqa: F401 import shelfmark.config.settings
import shelfmark.release_sources.irc.settings # noqa: F401 import shelfmark.metadata_providers.googlebooks
import shelfmark.metadata_providers.hardcover
import shelfmark.metadata_providers.openlibrary
import shelfmark.release_sources.irc.settings
import shelfmark.release_sources.prowlarr.settings # noqa: F401 import shelfmark.release_sources.prowlarr.settings # noqa: F401
import shelfmark.metadata_providers.hardcover # noqa: F401
import shelfmark.metadata_providers.openlibrary # noqa: F401
import shelfmark.metadata_providers.googlebooks # noqa: F401
from shelfmark.core.settings_registry import ( from shelfmark.core.settings_registry import (
ActionButton,
HeadingField,
get_all_groups, get_all_groups,
get_all_settings_tabs, get_all_settings_tabs,
) )
@@ -240,7 +236,7 @@ def generate_env_docs() -> str:
groups = {g.name: g for g in get_all_groups()} groups = {g.name: g for g in get_all_groups()}
# Organize tabs by group # Organize tabs by group
grouped_tabs: Dict[Optional[str], List] = {None: []} grouped_tabs: dict[str | None, list] = {None: []}
for group_name in groups: for group_name in groups:
grouped_tabs[group_name] = [] grouped_tabs[group_name] = []
@@ -309,7 +305,7 @@ def generate_env_docs() -> str:
return "\n".join(lines) return "\n".join(lines)
def _generate_tab_docs(tab, group_prefix: Optional[str] = None) -> List[str]: def _generate_tab_docs(tab: Any, group_prefix: str | None = None) -> list[str]:
"""Generate documentation for a single settings tab.""" """Generate documentation for a single settings tab."""
from shelfmark.core.settings_registry import ActionButton, CustomComponentField, HeadingField from shelfmark.core.settings_registry import ActionButton, CustomComponentField, HeadingField
@@ -318,7 +314,6 @@ def _generate_tab_docs(tab, group_prefix: Optional[str] = None) -> List[str]:
# Section header # Section header
if group_prefix: if group_prefix:
lines.append(f"### {group_prefix}: {tab.display_name}") lines.append(f"### {group_prefix}: {tab.display_name}")
anchor_id = f"{group_prefix}-{tab.display_name}".lower().replace(" ", "-")
else: else:
lines.append(f"## {tab.display_name}") lines.append(f"## {tab.display_name}")
@@ -391,6 +386,7 @@ def _generate_tab_docs(tab, group_prefix: Optional[str] = None) -> List[str]:
# Show constraints for NumberField # Show constraints for NumberField
from shelfmark.core.settings_registry import NumberField from shelfmark.core.settings_registry import NumberField
if isinstance(field, NumberField): if isinstance(field, NumberField):
constraints = [] constraints = []
if field.min_value is not None: if field.min_value is not None:
@@ -408,7 +404,7 @@ def _generate_tab_docs(tab, group_prefix: Optional[str] = None) -> List[str]:
return lines return lines
def main(): def main() -> None:
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(
description="Generate markdown documentation for environment variables" description="Generate markdown documentation for environment variables"
) )
+34 -23
View File
@@ -51,7 +51,8 @@ First-Time Setup:
import sys import sys
import time import time
from xmlrpc import client from pathlib import Path
from typing import Any
# Test configuration - matches docker-compose.test-clients.yml # Test configuration - matches docker-compose.test-clients.yml
CONFIG = { CONFIG = {
@@ -89,7 +90,7 @@ CONFIG = {
TEST_MAGNET = "magnet:?xt=urn:btih:3b245504cf5f11bbdbe1201cea6a6bf45aee1bc0&dn=ubuntu-22.04.3-live-server-amd64.iso" TEST_MAGNET = "magnet:?xt=urn:btih:3b245504cf5f11bbdbe1201cea6a6bf45aee1bc0&dn=ubuntu-22.04.3-live-server-amd64.iso"
def test_nzbget(): def test_nzbget() -> bool:
"""Test NZBGet connection.""" """Test NZBGet connection."""
import requests import requests
@@ -138,7 +139,7 @@ def test_nzbget():
return False return False
def test_sabnzbd(): def test_sabnzbd() -> bool:
"""Test SABnzbd connection.""" """Test SABnzbd connection."""
import requests import requests
@@ -152,10 +153,9 @@ def test_sabnzbd():
# Try to get API key from config if not set # Try to get API key from config if not set
if not api_key: if not api_key:
try: try:
import os ini_path = Path(".local/test-clients/sabnzbd/config/sabnzbd.ini")
ini_path = ".local/test-clients/sabnzbd/config/sabnzbd.ini" if ini_path.exists():
if os.path.exists(ini_path): with ini_path.open() as f:
with open(ini_path) as f:
for line in f: for line in f:
if line.startswith("api_key"): if line.startswith("api_key"):
api_key = line.split("=")[1].strip() api_key = line.split("=")[1].strip()
@@ -204,7 +204,7 @@ def test_sabnzbd():
return False return False
def test_qbittorrent(): def test_qbittorrent() -> bool:
"""Test qBittorrent connection.""" """Test qBittorrent connection."""
print("\n" + "=" * 50) print("\n" + "=" * 50)
print("Testing qBittorrent") print("Testing qBittorrent")
@@ -219,6 +219,7 @@ def test_qbittorrent():
# Parse URL for host/port # Parse URL for host/port
from urllib.parse import urlparse from urllib.parse import urlparse
parsed = urlparse(url) parsed = urlparse(url)
client = qbittorrentapi.Client( client = qbittorrentapi.Client(
@@ -271,16 +272,17 @@ def test_qbittorrent():
return False return False
def test_transmission(): def test_transmission() -> bool:
"""Test Transmission connection.""" """Test Transmission connection."""
print("\n" + "=" * 50) print("\n" + "=" * 50)
print("Testing Transmission") print("Testing Transmission")
print("=" * 50) print("=" * 50)
try: try:
from transmission_rpc import Client
from urllib.parse import urlparse from urllib.parse import urlparse
from transmission_rpc import Client
url = CONFIG["transmission"]["url"] url = CONFIG["transmission"]["url"]
parsed = urlparse(url) parsed = urlparse(url)
@@ -324,7 +326,7 @@ def test_transmission():
return False return False
def test_deluge(): def test_deluge() -> bool:
"""Test Deluge Web UI (JSON-RPC) connection.""" """Test Deluge Web UI (JSON-RPC) connection."""
import requests import requests
@@ -336,7 +338,7 @@ def test_deluge():
password = CONFIG["deluge"]["password"] password = CONFIG["deluge"]["password"]
rpc_url = f"{base_url}/json" rpc_url = f"{base_url}/json"
def rpc_call(session: requests.Session, rpc_id: int, method: str, *params): def rpc_call(session: requests.Session, rpc_id: int, method: str, *params: Any) -> Any:
payload = {"id": rpc_id, "method": method, "params": list(params)} payload = {"id": rpc_id, "method": method, "params": list(params)}
resp = session.post(rpc_url, json=payload, timeout=10) resp = session.post(rpc_url, json=payload, timeout=10)
resp.raise_for_status() resp.raise_for_status()
@@ -366,7 +368,11 @@ def test_deluge():
host_id = hosts[0][0] host_id = hosts[0][0]
for entry in hosts: for entry in hosts:
if isinstance(entry, list) and len(entry) >= 2 and entry[1] in {"127.0.0.1", "localhost"}: if (
isinstance(entry, list)
and len(entry) >= 2
and entry[1] in {"127.0.0.1", "localhost"}
):
host_id = entry[0] host_id = entry[0]
break break
@@ -386,13 +392,18 @@ def test_deluge():
# Test adding a torrent (then remove it) # Test adding a torrent (then remove it)
print(" Testing add/remove torrent...") print(" Testing add/remove torrent...")
torrent_id = rpc_call(session, 8, "core.add_torrent_magnet", TEST_MAGNET, {"add_paused": True}) torrent_id = rpc_call(
session, 8, "core.add_torrent_magnet", TEST_MAGNET, {"add_paused": True}
)
if torrent_id: if torrent_id:
torrent_id = str(torrent_id) torrent_id = str(torrent_id)
print(f" Added test torrent: {torrent_id[:20]}...") print(f" Added test torrent: {torrent_id[:20]}...")
status = rpc_call(session, 9, "core.get_torrent_status", torrent_id, ["state", "progress"]) or {} status = (
rpc_call(session, 9, "core.get_torrent_status", torrent_id, ["state", "progress"])
or {}
)
state = status.get("state", "unknown") if isinstance(status, dict) else "unknown" state = status.get("state", "unknown") if isinstance(status, dict) else "unknown"
progress = status.get("progress", 0) if isinstance(status, dict) else 0 progress = status.get("progress", 0) if isinstance(status, dict) else 0
print(f" Status: {state} ({progress:.1f}%)") print(f" Status: {state} ({progress:.1f}%)")
@@ -418,7 +429,8 @@ def test_deluge():
print(" Check Deluge Web UI password (default: deluge)") print(" Check Deluge Web UI password (default: deluge)")
return False return False
def test_rtorrent():
def test_rtorrent() -> bool:
"""Test rTorrent connection.""" """Test rTorrent connection."""
print("\n" + "=" * 50) print("\n" + "=" * 50)
print("Testing rTorrent") print("Testing rTorrent")
@@ -457,19 +469,18 @@ def test_rtorrent():
# rtorrent is weird in that it doesn't return the torrent ID/hash on add # rtorrent is weird in that it doesn't return the torrent ID/hash on add
client.load.start("", TEST_MAGNET, ";".join(commands)) client.load.start("", TEST_MAGNET, ";".join(commands))
# but we know that it is 3b245504cf5f11bbdbe1201cea6a6bf45aee1bc0 from the magnet link # but we know that it is 3b245504cf5f11bbdbe1201cea6a6bf45aee1bc0 from the magnet link
torrent_id = "3B245504CF5F11BBDBE1201CEA6A6BF45AEE1BC0" # rtorrent uses uppercase hashes torrent_id = "3B245504CF5F11BBDBE1201CEA6A6BF45AEE1BC0" # rtorrent uses uppercase hashes
print(f" Added test torrent: {torrent_id}") print(f" Added test torrent: {torrent_id}")
torrents = client.download_list() torrents = client.download_list()
print(f" Active torrents: {len(torrents)}") print(f" Active torrents: {len(torrents)}")
torrent_list = client.d.multicall.filtered( torrent_list = client.d.multicall.filtered(
"", "",
"default", "default",
f"equal={{d.hash=,cat={torrent_id}}}" f"equal={{d.hash=,cat={torrent_id}}}d.hash=",
"d.hash=",
"d.state=", "d.state=",
"d.completed_bytes=", "d.completed_bytes=",
"d.size_bytes=", "d.size_bytes=",
@@ -483,7 +494,7 @@ def test_rtorrent():
if not torrent: if not torrent:
print(" ERROR: Could not find added torrent in list") print(" ERROR: Could not find added torrent in list")
return False return False
# let's test the base path call # let's test the base path call
details = client.d.multicall.filtered( details = client.d.multicall.filtered(
"", "",
@@ -511,7 +522,7 @@ def test_rtorrent():
return False return False
def main(): def main() -> int:
print("Download Client Test Suite") print("Download Client Test Suite")
print("=" * 50) print("=" * 50)
print("Make sure containers are running:") print("Make sure containers are running:")
-1
View File
@@ -96,4 +96,3 @@ make typecheck
- Clear `node_modules` and reinstall: `make clean && make install` - Clear `node_modules` and reinstall: `make clean && make install`
- Check Node.js version compatibility - Check Node.js version compatibility
- Verify TypeScript configuration - Verify TypeScript configuration
+3 -3
View File
@@ -5,16 +5,16 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" /> <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
<meta name="description" content="Shelfmark - Book search and download" /> <meta name="description" content="Shelfmark - Book search and download" />
<base href="/" data-shelfmark-base /> <base href="/" data-shelfmark-base />
<!-- Theme color with media queries for light/dark mode --> <!-- Theme color with media queries for light/dark mode -->
<meta name="theme-color" content="#f8f8f8" media="(prefers-color-scheme: light)" /> <meta name="theme-color" content="#f8f8f8" media="(prefers-color-scheme: light)" />
<meta name="theme-color" content="#121212" media="(prefers-color-scheme: dark)" /> <meta name="theme-color" content="#121212" media="(prefers-color-scheme: dark)" />
<!-- iOS PWA Meta Tags --> <!-- iOS PWA Meta Tags -->
<meta name="apple-mobile-web-app-capable" content="yes" /> <meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" /> <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-title" content="Shelfmark" /> <meta name="apple-mobile-web-app-title" content="Shelfmark" />
<!-- App Icons --> <!-- App Icons -->
<link rel="icon" type="image/x-icon" href="favicon.ico" /> <link rel="icon" type="image/x-icon" href="favicon.ico" />
<link rel="apple-touch-icon" href="logo.png" /> <link rel="apple-touch-icon" href="logo.png" />
+1 -1
View File
@@ -421,7 +421,7 @@ export const Header = forwardRef<HeaderHandle, HeaderProps>(({
}} }}
> >
<div className="py-1"> <div className="py-1">
<a <a
href="https://github.com/calibrain/shelfmark/issues" href="https://github.com/calibrain/shelfmark/issues"
target="_blank" target="_blank"
@@ -121,5 +121,3 @@ export const LanguageMultiSelect = ({
/> />
); );
}; };
@@ -215,4 +215,3 @@ export const CardView = ({ book, onDetails, onDownload, onGetReleases, buttonSta
</article> </article>
); );
}; };
@@ -219,4 +219,3 @@ export const CompactView = ({ book, onDetails, onDownload, onGetReleases, button
</article> </article>
); );
}; };
@@ -274,4 +274,3 @@ export const ListView = ({ books, onDetails, onDownload, onGetReleases, getButto
</article> </article>
); );
}; };
-1
View File
@@ -24,4 +24,3 @@ export const CONTENT_OPTIONS = [
{ value: 'other', label: 'Other' }, { value: 'other', label: 'Other' },
{ value: 'musical_score', label: 'Musical score' }, { value: 'musical_score', label: 'Musical score' },
]; ];
+2 -2
View File
@@ -7,13 +7,13 @@ export const useToast = () => {
const showToast = useCallback((message: string, type: 'info' | 'success' | 'error' = 'info', persistent: boolean = false): string => { const showToast = useCallback((message: string, type: 'info' | 'success' | 'error' = 'info', persistent: boolean = false): string => {
const id = Date.now().toString(); const id = Date.now().toString();
setToasts(prev => [...prev, { id, message, type }]); setToasts(prev => [...prev, { id, message, type }]);
if (!persistent) { if (!persistent) {
setTimeout(() => { setTimeout(() => {
setToasts(prev => prev.filter(t => t.id !== id)); setToasts(prev => prev.filter(t => t.id !== id));
}, 4000); }, 4000);
} }
return id; return id;
}, []); }, []);
-1
View File
@@ -38,4 +38,3 @@ export const LoginPage = ({ onLogin, error, isLoading, authMode, oidcButtonLabel
</div> </div>
); );
}; };
-1
View File
@@ -8,4 +8,3 @@ export class UserCancelledError extends Error {
export function isUserCancelledError(error: unknown): error is UserCancelledError { export function isUserCancelledError(error: unknown): error is UserCancelledError {
return error instanceof UserCancelledError; return error instanceof UserCancelledError;
} }
+4 -4
View File
@@ -31,18 +31,18 @@ set -e
# Check if EXT_BYPASSER_URL is defined # Check if EXT_BYPASSER_URL is defined
if [ -n "$EXT_BYPASSER_URL" ]; then if [ -n "$EXT_BYPASSER_URL" ]; then
echo "Extracting hostname and ip from bypasser into /etc/hosts" echo "Extracting hostname and ip from bypasser into /etc/hosts"
# Extract hostname # Extract hostname
hostname=$(echo "$EXT_BYPASSER_URL" | cut -d'/' -f3 | cut -d':' -f1) hostname=$(echo "$EXT_BYPASSER_URL" | cut -d'/' -f3 | cut -d':' -f1)
# Resolve to IP (using current DNS before switching to TOR) # Resolve to IP (using current DNS before switching to TOR)
ip=$(getent hosts "$hostname" 2>/dev/null | awk '{print $1}') ip=$(getent hosts "$hostname" 2>/dev/null | awk '{print $1}')
# If getent fails, try dig # If getent fails, try dig
if [ -z "$ip" ]; then if [ -z "$ip" ]; then
ip=$(dig +short "$hostname" 2>/dev/null | head -n1) ip=$(dig +short "$hostname" 2>/dev/null | head -n1)
fi fi
# Only proceed if we got an IP and hostname is not already an IP # Only proceed if we got an IP and hostname is not already an IP
if [ -n "$ip" ] && [ "$ip" != "$hostname" ]; then if [ -n "$ip" ] && [ "$ip" != "$hostname" ]; then
# Add to /etc/hosts (remove existing entry first to avoid duplicates) # Add to /etc/hosts (remove existing entry first to avoid duplicates)
Generated
+81
View File
@@ -202,6 +202,45 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
] ]
[[package]]
name = "coverage"
version = "7.13.5"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/9d/e0/70553e3000e345daff267cec284ce4cbf3fc141b6da229ac52775b5428f1/coverage-7.13.5.tar.gz", hash = "sha256:c81f6515c4c40141f83f502b07bbfa5c240ba25bbe73da7b33f1e5b6120ff179", size = 915967, upload-time = "2026-03-17T10:33:18.341Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/8e/77/39703f0d1d4b478bfd30191d3c14f53caf596fac00efb3f8f6ee23646439/coverage-7.13.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fbabfaceaeb587e16f7008f7795cd80d20ec548dc7f94fbb0d4ec2e038ce563f", size = 219621, upload-time = "2026-03-17T10:32:08.589Z" },
{ url = "https://files.pythonhosted.org/packages/e2/3e/51dff36d99ae14639a133d9b164d63e628532e2974d8b1edb99dd1ebc733/coverage-7.13.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9bb2a28101a443669a423b665939381084412b81c3f8c0fcfbac57f4e30b5b8e", size = 219953, upload-time = "2026-03-17T10:32:10.507Z" },
{ url = "https://files.pythonhosted.org/packages/6a/6c/1f1917b01eb647c2f2adc9962bd66c79eb978951cab61bdc1acab3290c07/coverage-7.13.5-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:bd3a2fbc1c6cccb3c5106140d87cc6a8715110373ef42b63cf5aea29df8c217a", size = 250992, upload-time = "2026-03-17T10:32:12.41Z" },
{ url = "https://files.pythonhosted.org/packages/22/e5/06b1f88f42a5a99df42ce61208bdec3bddb3d261412874280a19796fc09c/coverage-7.13.5-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6c36ddb64ed9d7e496028d1d00dfec3e428e0aabf4006583bb1839958d280510", size = 253503, upload-time = "2026-03-17T10:32:14.449Z" },
{ url = "https://files.pythonhosted.org/packages/80/28/2a148a51e5907e504fa7b85490277734e6771d8844ebcc48764a15e28155/coverage-7.13.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:380e8e9084d8eb38db3a9176a1a4f3c0082c3806fa0dc882d1d87abc3c789247", size = 254852, upload-time = "2026-03-17T10:32:16.56Z" },
{ url = "https://files.pythonhosted.org/packages/61/77/50e8d3d85cc0b7ebe09f30f151d670e302c7ff4a1bf6243f71dd8b0981fa/coverage-7.13.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e808af52a0513762df4d945ea164a24b37f2f518cbe97e03deaa0ee66139b4d6", size = 257161, upload-time = "2026-03-17T10:32:19.004Z" },
{ url = "https://files.pythonhosted.org/packages/3b/c4/b5fd1d4b7bf8d0e75d997afd3925c59ba629fc8616f1b3aae7605132e256/coverage-7.13.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e301d30dd7e95ae068671d746ba8c34e945a82682e62918e41b2679acd2051a0", size = 251021, upload-time = "2026-03-17T10:32:21.344Z" },
{ url = "https://files.pythonhosted.org/packages/f8/66/6ea21f910e92d69ef0b1c3346ea5922a51bad4446c9126db2ae96ee24c4c/coverage-7.13.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:800bc829053c80d240a687ceeb927a94fd108bbdc68dfbe505d0d75ab578a882", size = 252858, upload-time = "2026-03-17T10:32:23.506Z" },
{ url = "https://files.pythonhosted.org/packages/9e/ea/879c83cb5d61aa2a35fb80e72715e92672daef8191b84911a643f533840c/coverage-7.13.5-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:0b67af5492adb31940ee418a5a655c28e48165da5afab8c7fa6fd72a142f8740", size = 250823, upload-time = "2026-03-17T10:32:25.516Z" },
{ url = "https://files.pythonhosted.org/packages/8a/fb/616d95d3adb88b9803b275580bdeee8bd1b69a886d057652521f83d7322f/coverage-7.13.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c9136ff29c3a91e25b1d1552b5308e53a1e0653a23e53b6366d7c2dcbbaf8a16", size = 255099, upload-time = "2026-03-17T10:32:27.944Z" },
{ url = "https://files.pythonhosted.org/packages/1c/93/25e6917c90ec1c9a56b0b26f6cad6408e5f13bb6b35d484a0d75c9cf000d/coverage-7.13.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:cff784eef7f0b8f6cb28804fbddcfa99f89efe4cc35fb5627e3ac58f91ed3ac0", size = 250638, upload-time = "2026-03-17T10:32:29.914Z" },
{ url = "https://files.pythonhosted.org/packages/fc/7b/dc1776b0464145a929deed214aef9fb1493f159b59ff3c7eeeedf91eddd0/coverage-7.13.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:68a4953be99b17ac3c23b6efbc8a38330d99680c9458927491d18700ef23ded0", size = 252295, upload-time = "2026-03-17T10:32:31.981Z" },
{ url = "https://files.pythonhosted.org/packages/ea/fb/99cbbc56a26e07762a2740713f3c8f9f3f3106e3a3dd8cc4474954bccd34/coverage-7.13.5-cp314-cp314-win32.whl", hash = "sha256:35a31f2b1578185fbe6aa2e74cea1b1d0bbf4c552774247d9160d29b80ed56cc", size = 222360, upload-time = "2026-03-17T10:32:34.233Z" },
{ url = "https://files.pythonhosted.org/packages/8d/b7/4758d4f73fb536347cc5e4ad63662f9d60ba9118cb6785e9616b2ce5d7fa/coverage-7.13.5-cp314-cp314-win_amd64.whl", hash = "sha256:2aa055ae1857258f9e0045be26a6d62bdb47a72448b62d7b55f4820f361a2633", size = 223174, upload-time = "2026-03-17T10:32:36.369Z" },
{ url = "https://files.pythonhosted.org/packages/2c/f2/24d84e1dfe70f8ac9fdf30d338239860d0d1d5da0bda528959d0ebc9da28/coverage-7.13.5-cp314-cp314-win_arm64.whl", hash = "sha256:1b11eef33edeae9d142f9b4358edb76273b3bfd30bc3df9a4f95d0e49caf94e8", size = 221739, upload-time = "2026-03-17T10:32:38.736Z" },
{ url = "https://files.pythonhosted.org/packages/60/5b/4a168591057b3668c2428bff25dd3ebc21b629d666d90bcdfa0217940e84/coverage-7.13.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:10a0c37f0b646eaff7cce1874c31d1f1ccb297688d4c747291f4f4c70741cc8b", size = 220351, upload-time = "2026-03-17T10:32:41.196Z" },
{ url = "https://files.pythonhosted.org/packages/f5/21/1fd5c4dbfe4a58b6b99649125635df46decdfd4a784c3cd6d410d303e370/coverage-7.13.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b5db73ba3c41c7008037fa731ad5459fc3944cb7452fc0aa9f822ad3533c583c", size = 220612, upload-time = "2026-03-17T10:32:43.204Z" },
{ url = "https://files.pythonhosted.org/packages/d6/fe/2a924b3055a5e7e4512655a9d4609781b0d62334fa0140c3e742926834e2/coverage-7.13.5-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:750db93a81e3e5a9831b534be7b1229df848b2e125a604fe6651e48aa070e5f9", size = 261985, upload-time = "2026-03-17T10:32:45.514Z" },
{ url = "https://files.pythonhosted.org/packages/d7/0d/c8928f2bd518c45990fe1a2ab8db42e914ef9b726c975facc4282578c3eb/coverage-7.13.5-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9ddb4f4a5479f2539644be484da179b653273bca1a323947d48ab107b3ed1f29", size = 264107, upload-time = "2026-03-17T10:32:47.971Z" },
{ url = "https://files.pythonhosted.org/packages/ef/ae/4ae35bbd9a0af9d820362751f0766582833c211224b38665c0f8de3d487f/coverage-7.13.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8a7a2049c14f413163e2bdabd37e41179b1d1ccb10ffc6ccc4b7a718429c607", size = 266513, upload-time = "2026-03-17T10:32:50.1Z" },
{ url = "https://files.pythonhosted.org/packages/9c/20/d326174c55af36f74eac6ae781612d9492f060ce8244b570bb9d50d9d609/coverage-7.13.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e1c85e0b6c05c592ea6d8768a66a254bfb3874b53774b12d4c89c481eb78cb90", size = 267650, upload-time = "2026-03-17T10:32:52.391Z" },
{ url = "https://files.pythonhosted.org/packages/7a/5e/31484d62cbd0eabd3412e30d74386ece4a0837d4f6c3040a653878bfc019/coverage-7.13.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:777c4d1eff1b67876139d24288aaf1817f6c03d6bae9c5cc8d27b83bcfe38fe3", size = 261089, upload-time = "2026-03-17T10:32:54.544Z" },
{ url = "https://files.pythonhosted.org/packages/e9/d8/49a72d6de146eebb0b7e48cc0f4bc2c0dd858e3d4790ab2b39a2872b62bd/coverage-7.13.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6697e29b93707167687543480a40f0db8f356e86d9f67ddf2e37e2dfd91a9dab", size = 263982, upload-time = "2026-03-17T10:32:56.803Z" },
{ url = "https://files.pythonhosted.org/packages/06/3b/0351f1bd566e6e4dd39e978efe7958bde1d32f879e85589de147654f57bb/coverage-7.13.5-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:8fdf453a942c3e4d99bd80088141c4c6960bb232c409d9c3558e2dbaa3998562", size = 261579, upload-time = "2026-03-17T10:32:59.466Z" },
{ url = "https://files.pythonhosted.org/packages/5d/ce/796a2a2f4017f554d7810f5c573449b35b1e46788424a548d4d19201b222/coverage-7.13.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:32ca0c0114c9834a43f045a87dcebd69d108d8ffb666957ea65aa132f50332e2", size = 265316, upload-time = "2026-03-17T10:33:01.847Z" },
{ url = "https://files.pythonhosted.org/packages/3d/16/d5ae91455541d1a78bc90abf495be600588aff8f6db5c8b0dae739fa39c9/coverage-7.13.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:8769751c10f339021e2638cd354e13adeac54004d1941119b2c96fe5276d45ea", size = 260427, upload-time = "2026-03-17T10:33:03.945Z" },
{ url = "https://files.pythonhosted.org/packages/48/11/07f413dba62db21fb3fad5d0de013a50e073cc4e2dc4306e770360f6dfc8/coverage-7.13.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cec2d83125531bd153175354055cdb7a09987af08a9430bd173c937c6d0fba2a", size = 262745, upload-time = "2026-03-17T10:33:06.285Z" },
{ url = "https://files.pythonhosted.org/packages/91/15/d792371332eb4663115becf4bad47e047d16234b1aff687b1b18c58d60ae/coverage-7.13.5-cp314-cp314t-win32.whl", hash = "sha256:0cd9ed7a8b181775459296e402ca4fb27db1279740a24e93b3b41942ebe4b215", size = 223146, upload-time = "2026-03-17T10:33:08.756Z" },
{ url = "https://files.pythonhosted.org/packages/db/51/37221f59a111dca5e85be7dbf09696323b5b9f13ff65e0641d535ed06ea8/coverage-7.13.5-cp314-cp314t-win_amd64.whl", hash = "sha256:301e3b7dfefecaca37c9f1aa6f0049b7d4ab8dd933742b607765d757aca77d43", size = 224254, upload-time = "2026-03-17T10:33:11.174Z" },
{ url = "https://files.pythonhosted.org/packages/54/83/6acacc889de8987441aa7d5adfbdbf33d288dad28704a67e574f1df9bcbb/coverage-7.13.5-cp314-cp314t-win_arm64.whl", hash = "sha256:9dacc2ad679b292709e0f5fc1ac74a6d4d5562e424058962c7bb0c658ad25e45", size = 222276, upload-time = "2026-03-17T10:33:13.466Z" },
{ url = "https://files.pythonhosted.org/packages/9e/ee/a4cf96b8ce1e566ed238f0659ac2d3f007ed1d14b181bcb684e19561a69a/coverage-7.13.5-py3-none-any.whl", hash = "sha256:34b02417cf070e173989b3db962f7ed56d2f644307b2cf9d5a0f258e13084a61", size = 211346, upload-time = "2026-03-17T10:33:15.691Z" },
]
[[package]] [[package]]
name = "cryptography" name = "cryptography"
version = "46.0.7" version = "46.0.7"
@@ -702,6 +741,30 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
] ]
[[package]]
name = "prek"
version = "0.3.8"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/62/ee/03e8180e3fda9de25b6480bd15cc2bde40d573868d50648b0e527b35562f/prek-0.3.8.tar.gz", hash = "sha256:434a214256516f187a3ab15f869d950243be66b94ad47987ee4281b69643a2d9", size = 400224, upload-time = "2026-03-23T08:23:35.981Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/00/84/40d2ddf362d12c4cd4a25a8c89a862edf87cdfbf1422aa41aac8e315d409/prek-0.3.8-py3-none-linux_armv6l.whl", hash = "sha256:6fb646ada60658fa6dd7771b2e0fb097f005151be222f869dada3eb26d79ed33", size = 5226646, upload-time = "2026-03-23T08:23:18.306Z" },
{ url = "https://files.pythonhosted.org/packages/e1/52/7308a033fa43b7e8e188797bd2b3b017c0f0adda70fa7af575b1f43ea888/prek-0.3.8-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:f3d7fdadb15efc19c09953c7a33cf2061a70f367d1e1957358d3ad5cc49d0616", size = 5620104, upload-time = "2026-03-23T08:23:40.053Z" },
{ url = "https://files.pythonhosted.org/packages/ff/b1/f106ac000a91511a9cd80169868daf2f5b693480ef5232cec5517a38a512/prek-0.3.8-py3-none-macosx_11_0_arm64.whl", hash = "sha256:72728c3295e79ca443f8c1ec037d2a5b914ec73a358f69cf1bc1964511876bf8", size = 5199867, upload-time = "2026-03-23T08:23:38.066Z" },
{ url = "https://files.pythonhosted.org/packages/b3/e9/970713f4b019f69de9844e1bab37b8ddb67558e410916f4eb5869a696165/prek-0.3.8-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:48efc28f2f53b5b8087efca9daaed91572d62df97d5f24a1c7a087fecb5017de", size = 5441801, upload-time = "2026-03-23T08:23:32.617Z" },
{ url = "https://files.pythonhosted.org/packages/12/a4/7ef44032b181753e19452ec3b09abb3a32607cf6b0a0508f0604becaaf2b/prek-0.3.8-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f6ca9d63bacbc448a5c18e955c78d3ac5176c3a17c3baacdd949b1a623e08a36", size = 5155107, upload-time = "2026-03-23T08:23:31.021Z" },
{ url = "https://files.pythonhosted.org/packages/bd/77/4d9c8985dbba84149760785dfe07093ea1e29d710257dfb7c89615e2234c/prek-0.3.8-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1000f7029696b4fe712fb1fefd4c55b9c4de72b65509c8e50296370a06f9dc3f", size = 5566541, upload-time = "2026-03-23T08:23:45.694Z" },
{ url = "https://files.pythonhosted.org/packages/1a/1a/81e6769ac1f7f8346d09ce2ab0b47cf06466acd9ff72e87e5d1f0d98cd32/prek-0.3.8-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6ff0bed0e2c1286522987d982168a86cbbd0d069d840506a46c9fda983515517", size = 6552991, upload-time = "2026-03-23T08:23:21.958Z" },
{ url = "https://files.pythonhosted.org/packages/6f/fa/ce2df0dd2dc75a9437a52463239d0782998943d7b04e191fb89b83016c34/prek-0.3.8-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4fb087ac0ffda3ac65bbbae9a38326a7fd27ee007bb4a94323ce1eb539d8bbec", size = 5832972, upload-time = "2026-03-23T08:23:20.258Z" },
{ url = "https://files.pythonhosted.org/packages/18/6b/9d4269df9073216d296244595a21c253b6475dfc9076c0bd2906be7a436c/prek-0.3.8-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:2e1e5e206ff7b31bd079cce525daddc96cd6bc544d20dc128921ad92f7a4c85d", size = 5448371, upload-time = "2026-03-23T08:23:41.835Z" },
{ url = "https://files.pythonhosted.org/packages/60/1d/1e4d8a78abefa5b9d086e5a9f1638a74b5e540eec8a648d9946707701f29/prek-0.3.8-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:dcea3fe23832a4481bccb7c45f55650cb233be7c805602e788bb7dba60f2d861", size = 5270546, upload-time = "2026-03-23T08:23:24.231Z" },
{ url = "https://files.pythonhosted.org/packages/77/07/34f36551a6319ae36e272bea63a42f59d41d2d47ab0d5fb00eb7b4e88e87/prek-0.3.8-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:4d25e647e9682f6818ab5c31e7a4b842993c14782a6ffcd128d22b784e0d677f", size = 5124032, upload-time = "2026-03-23T08:23:26.368Z" },
{ url = "https://files.pythonhosted.org/packages/e3/01/6d544009bb655e709993411796af77339f439526db4f3b3509c583ad8eb9/prek-0.3.8-py3-none-musllinux_1_1_i686.whl", hash = "sha256:de528b82935e33074815acff3c7c86026754d1212136295bc88fe9c43b4231d5", size = 5432245, upload-time = "2026-03-23T08:23:47.877Z" },
{ url = "https://files.pythonhosted.org/packages/54/96/1237ee269e9bfa283ffadbcba1f401f48a47aed2b2563eb1002740d6079d/prek-0.3.8-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:6d660f1c25a126e6d9f682fe61449441226514f412a4469f5d71f8f8cad56db2", size = 5950550, upload-time = "2026-03-23T08:23:43.8Z" },
{ url = "https://files.pythonhosted.org/packages/ca/6b/a574411459049bc691047c9912f375deda10c44a707b6ce98df2b658f0b3/prek-0.3.8-py3-none-win32.whl", hash = "sha256:b0c291c577615d9f8450421dff0b32bfd77a6b0d223ee4115a1f820cb636fdf1", size = 4949501, upload-time = "2026-03-23T08:23:16.338Z" },
{ url = "https://files.pythonhosted.org/packages/0c/b4/46b59fe49f635acd9f6530778ce577f9d8b49452835726a5311ffc902c67/prek-0.3.8-py3-none-win_amd64.whl", hash = "sha256:bc147fdbdd4ec33fc7a987b893ecb69b1413ac100d95c9889a70f3fd58c73d06", size = 5346551, upload-time = "2026-03-23T08:23:34.501Z" },
{ url = "https://files.pythonhosted.org/packages/53/05/9cca1708bb8c65264124eb4b04251e0f65ce5bfc707080bb6b492d5a0df7/prek-0.3.8-py3-none-win_arm64.whl", hash = "sha256:a2614647aeafa817a5802ccb9561e92eedc20dcf840639a1b00826e2c2442515", size = 5190872, upload-time = "2026-03-23T08:23:29.463Z" },
]
[[package]] [[package]]
name = "psutil" name = "psutil"
version = "7.2.2" version = "7.2.2"
@@ -886,6 +949,20 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" },
] ]
[[package]]
name = "pytest-cov"
version = "7.1.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "coverage" },
{ name = "pluggy" },
{ name = "pytest" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b1/51/a849f96e117386044471c8ec2bd6cfebacda285da9525c9106aeb28da671/pytest_cov-7.1.0.tar.gz", hash = "sha256:30674f2b5f6351aa09702a9c8c364f6a01c27aae0c1366ae8016160d1efc56b2", size = 55592, upload-time = "2026-03-21T20:11:16.284Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/9d/7a/d968e294073affff457b041c2be9868a40c1c71f4a35fcc1e45e5493067b/pytest_cov-7.1.0-py3-none-any.whl", hash = "sha256:a0461110b7865f9a271aa1b51e516c9a95de9d696734a2f71e3e78f46e1d4678", size = 22876, upload-time = "2026-03-21T20:11:14.438Z" },
]
[[package]] [[package]]
name = "pytest-html" name = "pytest-html"
version = "4.0.2" version = "4.0.2"
@@ -1280,7 +1357,9 @@ browser = [
[package.dev-dependencies] [package.dev-dependencies]
dev = [ dev = [
{ name = "basedpyright" }, { name = "basedpyright" },
{ name = "prek" },
{ name = "pytest" }, { name = "pytest" },
{ name = "pytest-cov" },
{ name = "pytest-xdist" }, { name = "pytest-xdist" },
{ name = "ruff" }, { name = "ruff" },
{ name = "vulture" }, { name = "vulture" },
@@ -1317,7 +1396,9 @@ provides-extras = ["browser"]
[package.metadata.requires-dev] [package.metadata.requires-dev]
dev = [ dev = [
{ name = "basedpyright", specifier = ">=1.39.0" }, { name = "basedpyright", specifier = ">=1.39.0" },
{ name = "prek" },
{ name = "pytest" }, { name = "pytest" },
{ name = "pytest-cov" },
{ name = "pytest-xdist", specifier = ">=3.8.0" }, { name = "pytest-xdist", specifier = ">=3.8.0" },
{ name = "ruff", specifier = "==0.15.10" }, { name = "ruff", specifier = "==0.15.10" },
{ name = "vulture", specifier = ">=2.14" }, { name = "vulture", specifier = ">=2.14" },