Compare commits

...
Author SHA1 Message Date
github-actions[bot] 8bd69a0f0e release: v0.9.35rc67 2026-06-21 15:03:28 +00:00
Nick Sweeting a350f58e44 Mark hook subprocesses as abxpkg scripts 2026-06-21 08:03:00 -07:00
github-actions[bot] c35c808da8 release: v0.9.35rc66 2026-06-21 14:51:10 +00:00
Nick Sweeting 82724e99dc Tighten public preview screenshot assertions 2026-06-21 07:50:45 -07:00
github-actions[bot] ce8a4790ae release: v0.9.35rc65 2026-06-21 14:31:03 +00:00
Nick Sweeting 614a5bd095 Prefer explicit hook library overrides 2026-06-21 07:30:32 -07:00
github-actions[bot] 215f7f5925 release: v0.9.35rc64 2026-06-21 14:10:05 +00:00
Nick Sweeting 74399c5456 Use maintenance runner for retention cleanup test 2026-06-21 07:09:35 -07:00
github-actions[bot] 284a9fde3d release: v0.9.35rc63 2026-06-21 13:50:09 +00:00
Nick Sweeting fee4bf13cb Handle sealed crawl pause race in API test 2026-06-21 06:49:38 -07:00
github-actions[bot] be9c77320c release: v0.9.35rc62 2026-06-21 13:37:31 +00:00
Nick Sweeting 1f0b2795bb Treat Chrome zombie cleanup as nonfatal 2026-06-21 06:37:00 -07:00
github-actions[bot] 6382722d2f release: v0.9.35rc61 2026-06-21 13:26:23 +00:00
Nick Sweeting 4be16fc00e Pin nonfatal Chrome cleanup plugin 2026-06-21 06:25:50 -07:00
github-actions[bot] dcccafc00e release: v0.9.35rc60 2026-06-21 13:09:33 +00:00
Nick Sweeting 3fa4fd5d18 Fix runtime hook config regressions 2026-06-21 06:09:04 -07:00
github-actions[bot] ef6f616b37 release: v0.9.35rc59 2026-06-21 12:42:53 +00:00
Nick Sweeting 0e0759a680 Skip binary probes outside data dirs 2026-06-21 05:42:26 -07:00
github-actions[bot] 947c340e74 release: v0.9.35rc58 2026-06-21 12:33:06 +00:00
Nick Sweeting 6f6eb92a90 Pin abx-plugins test fixes 2026-06-21 05:32:42 -07:00
github-actions[bot] 0064e20805 release: v0.9.35rc57 2026-06-21 12:24:51 +00:00
Nick Sweeting 59a956bf9f Show dynamic DB binaries in version output 2026-06-21 05:24:15 -07:00
github-actions[bot] afd2fac50b release: v0.9.35rc56 2026-06-21 12:01:57 +00:00
Nick Sweeting edb6a263e9 Disable package age gate in parallel CI installs 2026-06-21 05:01:33 -07:00
github-actions[bot] 1c00719f68 release: v0.9.35rc55 2026-06-21 11:55:23 +00:00
Nick Sweeting e30a4f2a1c Use fixed abx-plugins daemon test tag in CI 2026-06-21 04:54:45 -07:00
github-actions[bot] ba198e000b release: v0.9.35rc54 2026-06-21 11:38:12 +00:00
Nick Sweeting 24ed99a65d Use fixed abx-plugins cookie tag in CI 2026-06-21 04:37:46 -07:00
github-actions[bot] 2cc0035a40 release: v0.9.35rc53 2026-06-21 11:23:24 +00:00
Nick Sweeting b41f12ee38 Use fixed abx-plugins screenshot tag in CI 2026-06-21 04:22:48 -07:00
github-actions[bot] 714caded9c release: v0.9.35rc52 2026-06-21 11:15:03 +00:00
Nick Sweeting 200da8a167 Use fixed abx-plugins ublock tag in CI 2026-06-21 04:14:37 -07:00
github-actions[bot] 9b02796049 release: v0.9.35rc51 2026-06-21 10:33:57 +00:00
Nick Sweeting 062e4e12fd Use fixed abxpkg tag in CI 2026-06-21 03:33:20 -07:00
github-actions[bot] 8d9702b423 release: v0.9.35rc50 2026-06-21 10:20:24 +00:00
Nick Sweeting ec728caeab Use fixed abx-plugins tag in CI 2026-06-21 03:19:45 -07:00
Nick Sweeting 7bee5417e7 Stabilize parallel plugin CI setup 2026-06-21 03:18:18 -07:00
github-actions[bot] 580b49ab14 release: v0.9.35rc49 2026-06-21 10:03:04 +00:00
Nick Sweeting 128e6aeff1 pin CI helper repo clones 2026-06-21 03:02:37 -07:00
github-actions[bot] d8f0e0b472 release: v0.9.35rc48 2026-06-21 09:53:59 +00:00
Nick Sweeting 76da863a02 fix docker image commit metadata 2026-06-21 02:53:28 -07:00
github-actions[bot] a0d032ead3 release: v0.9.35rc47 2026-06-21 09:41:45 +00:00
Nick Sweeting 9dbeece35b fix version command exit status 2026-06-21 02:41:18 -07:00
github-actions[bot] 66835dcfb8 release: v0.9.35rc46 2026-06-21 09:30:21 +00:00
Nick Sweeting d259ac8095 fix machine network interface identity 2026-06-21 02:29:53 -07:00
Nick Sweeting 00e818018a release: v0.9.35rc46 2026-06-21 01:44:30 -07:00
github-actions[bot] bbc12762c0 release: v0.9.35rc45 2026-06-21 02:17:03 +00:00
Nick Sweeting aa2df34d02 Update SECURITY.md with XSS/CSRF information
Clarify security risks and update information on CSRF/XSS protections.
2026-06-20 22:16:38 -04:00
27 changed files with 555 additions and 121 deletions

No files matched your search

+2
View File
@@ -67,3 +67,5 @@ Non-logged in users should be tightly limited to prevent attacks like SQL/shell
`< v0.9.x` versions do not include any XSS/CSRF protections at all, as documented in the [readme](https://github.com/ArchiveBox/ArchiveBox/tree/v0.7.4#security-risks-of-viewing-archived-js), [wiki](https://github.com/ArchiveBox/ArchiveBox/wiki/Publishing-Your-Archive#security-concerns), [issue #239](https://github.com/ArchiveBox/ArchiveBox/issues/239), and [existing advisory](https://github.com/ArchiveBox/ArchiveBox/security/advisories/GHSA-cr45-98w9-gwqx). Do not open advisories related to XSS or CSRF for those versions, as they are missing fundamental architectural changes added in `v0.9.0` needed to support per-snapshot replay isolation.
[`SERVER_SECURITY_MODE`](https://github.com/archiveBox/archiveBox/wiki/configuration#server_security_mode) + CSRF/XSS defenses were only added in the new 0.9.0 `dev` work and are planned to land in `main` in mid-2026.
Note archivebox is designed to be able to archive any URL the server can reach, including internal intranet URLs. The only URLs that are blocked are URLs hosted by the archivebox server itself.
+37
View File
@@ -0,0 +1,37 @@
#!/usr/bin/env bash
set -Eeuo pipefail
repo_name="$1"
target_dir="${2:-$repo_name}"
case "$repo_name" in
abxpkg)
version="1.11.230"
;;
abx-plugins)
version="1.11.238"
;;
*)
version="$(
python3 - "$repo_name" <<'PY'
import re
import sys
from pathlib import Path
repo_name = sys.argv[1]
lock_text = Path("uv.lock").read_text()
match = re.search(
rf'^\[\[package\]\]\s*\nname = "{re.escape(repo_name)}"\s*\nversion = "([^"]+)"',
lock_text,
re.MULTILINE,
)
if not match:
raise SystemExit(f"Could not find {repo_name} in uv.lock")
print(match.group(1))
PY
)"
;;
esac
echo "Cloning ArchiveBox/${repo_name}@v${version} into ${target_dir}"
git clone --depth=1 --branch "v${version}" "https://github.com/ArchiveBox/${repo_name}.git" "$target_dir"
+1
View File
@@ -187,6 +187,7 @@ jobs:
labels: ${{ steps.docker_meta.outputs.labels }}
build-args: |
ABX_DL_IMAGE=${{ steps.abx_dl_image.outputs.image }}
ARCHIVEBOX_COMMIT_HASH=${{ github.sha }}
cache-from: type=gha,scope=${{ matrix.cache_scope }}
cache-to: type=gha,mode=max,scope=${{ matrix.cache_scope }}
pull: true
+3 -3
View File
@@ -22,13 +22,13 @@ jobs:
fetch-depth: 1
- name: Clone abxpkg
run: git clone --depth=1 https://github.com/ArchiveBox/abxpkg.git abxpkg
run: bash .github/scripts/clone_abx_repo.sh abxpkg
- name: Clone abx-plugins
run: git clone --depth=1 https://github.com/ArchiveBox/abx-plugins.git abx-plugins
run: bash .github/scripts/clone_abx_repo.sh abx-plugins
- name: Clone abx-dl
run: git clone --depth=1 https://github.com/ArchiveBox/abx-dl.git abx-dl
run: bash .github/scripts/clone_abx_repo.sh abx-dl
- name: Set up Python
uses: actions/setup-python@v5
+20 -7
View File
@@ -16,6 +16,7 @@ env:
PYTHONLEGACYWINDOWSSTDIO: utf-8
USE_COLOR: False
UV_NO_SOURCES: "1"
ABXPKG_MIN_RELEASE_AGE: "0"
jobs:
discover-tests:
@@ -67,7 +68,7 @@ jobs:
echo "$json_array" | jq '.'
- name: Clone abx-plugins
run: git clone --depth=1 https://github.com/ArchiveBox/abx-plugins.git abx-plugins
run: bash .github/scripts/clone_abx_repo.sh abx-plugins
- name: Discover plugin tests
id: set-plugin-matrix
@@ -124,13 +125,13 @@ jobs:
fetch-depth: 1
- name: Clone abxpkg
run: git clone --depth=1 https://github.com/ArchiveBox/abxpkg.git abxpkg
run: bash .github/scripts/clone_abx_repo.sh abxpkg
- name: Clone abx-plugins
run: git clone --depth=1 https://github.com/ArchiveBox/abx-plugins.git abx-plugins
run: bash .github/scripts/clone_abx_repo.sh abx-plugins
- name: Clone abx-dl
run: git clone --depth=1 https://github.com/ArchiveBox/abx-dl.git abx-dl
run: bash .github/scripts/clone_abx_repo.sh abx-dl
- name: Set up Python ${{ matrix.python }}
uses: actions/setup-python@v5
@@ -198,6 +199,12 @@ jobs:
fi
echo "Using ArchiveBox-installed Chromium: $candidate ($version)"
echo "CHROME_BINARY=$candidate" >> "$GITHUB_ENV"
echo "DATA_DIR=$ABX_CI_DATA_DIR" >> "$GITHUB_ENV"
echo "NODE_MODULES_DIR=$LIB_DIR/pnpm/packages/chrome/node_modules" >> "$GITHUB_ENV"
echo "NODE_PATH=$LIB_DIR/pnpm/packages/chrome/node_modules" >> "$GITHUB_ENV"
echo "PNPM_BIN_DIR=$LIB_DIR/pnpm/packages/chrome/node_modules/.bin" >> "$GITHUB_ENV"
echo "NPM_BIN_DIR=$LIB_DIR/pnpm/packages/chrome/node_modules/.bin" >> "$GITHUB_ENV"
echo "CHROMEWEBSTORE_EXTENSIONS_DIR=$LIB_DIR/chromewebstore/extensions" >> "$GITHUB_ENV"
- name: Install Sonic through ArchiveBox
if: ${{ matrix.test.needs_sonic }}
@@ -260,13 +267,13 @@ jobs:
fetch-depth: 1
- name: Clone abxpkg
run: git clone --depth=1 https://github.com/ArchiveBox/abxpkg.git abxpkg
run: bash .github/scripts/clone_abx_repo.sh abxpkg
- name: Clone abx-plugins
run: git clone --depth=1 https://github.com/ArchiveBox/abx-plugins.git abx-plugins
run: bash .github/scripts/clone_abx_repo.sh abx-plugins
- name: Clone abx-dl
run: git clone --depth=1 https://github.com/ArchiveBox/abx-dl.git abx-dl
run: bash .github/scripts/clone_abx_repo.sh abx-dl
- name: Set up Python 3.13
uses: actions/setup-python@v5
@@ -334,6 +341,12 @@ jobs:
fi
echo "Using ArchiveBox-installed Chromium: $candidate ($version)"
echo "CHROME_BINARY=$candidate" >> "$GITHUB_ENV"
echo "DATA_DIR=$ABX_CI_DATA_DIR" >> "$GITHUB_ENV"
echo "NODE_MODULES_DIR=$LIB_DIR/pnpm/packages/chrome/node_modules" >> "$GITHUB_ENV"
echo "NODE_PATH=$LIB_DIR/pnpm/packages/chrome/node_modules" >> "$GITHUB_ENV"
echo "PNPM_BIN_DIR=$LIB_DIR/pnpm/packages/chrome/node_modules/.bin" >> "$GITHUB_ENV"
echo "NPM_BIN_DIR=$LIB_DIR/pnpm/packages/chrome/node_modules/.bin" >> "$GITHUB_ENV"
echo "CHROMEWEBSTORE_EXTENSIONS_DIR=$LIB_DIR/chromewebstore/extensions" >> "$GITHUB_ENV"
- name: Install Sonic through ArchiveBox
if: ${{ matrix.plugin.needs_sonic }}
+3 -3
View File
@@ -31,13 +31,13 @@ jobs:
fetch-depth: 1
- name: Clone abxpkg
run: git clone --depth=1 https://github.com/ArchiveBox/abxpkg.git abxpkg
run: bash .github/scripts/clone_abx_repo.sh abxpkg
- name: Clone abx-plugins
run: git clone --depth=1 https://github.com/ArchiveBox/abx-plugins.git abx-plugins
run: bash .github/scripts/clone_abx_repo.sh abx-plugins
- name: Clone abx-dl
run: git clone --depth=1 https://github.com/ArchiveBox/abx-dl.git abx-dl
run: bash .github/scripts/clone_abx_repo.sh abx-dl
### Setup Python & JS Languages
- name: Set up Python ${{ matrix.python }}
+4 -1
View File
@@ -22,6 +22,7 @@ ARG TARGETPLATFORM
ARG TARGETOS
ARG TARGETARCH
ARG TARGETVARIANT
ARG ARCHIVEBOX_COMMIT_HASH=""
ENV TZ=UTC \
LANGUAGE=en_US:en \
@@ -165,7 +166,9 @@ COPY --chown=root:root --chmod=755 "." "$CODE_DIR/"
RUN --mount=type=cache,target=/root/.cache/uv,sharing=locked,id=uv-$TARGETARCH$TARGETVARIANT \
echo "[*] Installing ArchiveBox Python source code from $CODE_DIR..." \
&& COMMIT_HASH="$( \
if [[ -f "$CODE_DIR/.git/HEAD" ]]; then \
if [[ "$ARCHIVEBOX_COMMIT_HASH" =~ ^[0-9a-fA-F]{40}$ ]]; then \
echo "$ARCHIVEBOX_COMMIT_HASH"; \
elif [[ -f "$CODE_DIR/.git/HEAD" ]]; then \
HEAD_REF="$(cat "$CODE_DIR/.git/HEAD")"; \
if [[ "$HEAD_REF" =~ ^[0-9a-fA-F]{40}$ ]]; then \
echo "$HEAD_REF"; \
+2 -1
View File
@@ -4,6 +4,7 @@ import math
import json
import mimetypes
import re
from html import unescape
from collections import defaultdict
from pathlib import Path, PurePosixPath
from uuid import UUID
@@ -832,7 +833,7 @@ def _snapshots_rss_response(
crawl_user = snapshot.crawl.created_by if snapshot.crawl_id else None
description = f"Original URL: {snapshot.url}\nArchived snapshot: {archived_url}"
feed.add_item(
title=snapshot.title or snapshot.url,
title=unescape(snapshot.title or snapshot.url),
link=archived_url or web_base_url,
description=description,
unique_id=str(snapshot.id),
+102 -13
View File
@@ -82,19 +82,19 @@ def _build_binary_table(rows: list[dict[str, object]]):
from rich.table import Table
table = Table(title="Binary Dependencies", box=box.SIMPLE_HEAVY, expand=True)
table.add_column("Binary", no_wrap=True, max_width=28)
table.add_column("Plugin", no_wrap=True, max_width=24)
table.add_column("State", no_wrap=True, width=8)
table.add_column("Status", justify="center", no_wrap=True, width=6)
table.add_column("Binary", no_wrap=True, max_width=28)
table.add_column("Version", no_wrap=True, width=16)
table.add_column("Provider", no_wrap=True, width=8)
table.add_column("Path", overflow="fold", ratio=1)
for row in rows:
table.add_row(
str(row["binary"]),
str(row["plugin"]),
str(row["state"]),
str(row["status"]),
str(row["binary"]),
str(row["version"]),
str(row["provider"]),
row["path"],
@@ -106,10 +106,10 @@ def _build_binary_table(rows: list[dict[str, object]]):
def _print_binary_row(prnt, row: dict[str, object]) -> None:
prnt(
"",
str(row["binary"]).ljust(28),
row["status"],
str(row["plugin"]).ljust(24),
str(row["state"]).ljust(8),
str(row["binary"]).ljust(28),
str(row["version"]).ljust(16),
str(row["provider"]).ljust(8),
row["path"],
@@ -133,6 +133,23 @@ def _binary_record_matches_runtime(installed, lib_dir: Path) -> bool:
return True
def _binary_row_dedupe_key(
*,
display_name: str,
valid: bool,
version: str,
provider: str,
abspath: str,
) -> tuple[str, str, str, str]:
if not valid:
return (display_name, "", "", "")
try:
resolved_abspath = Path(abspath).expanduser().resolve(strict=False).as_posix()
except Exception:
resolved_abspath = abspath
return (display_name, provider, version, resolved_abspath)
@enforce_types
def version(
quiet: bool = False,
@@ -211,6 +228,11 @@ def version(
prnt()
in_data_dir = os.access(CONSTANTS.ARCHIVE_DIR, os.R_OK) and os.access(CONSTANTS.CONFIG_FILE, os.R_OK)
if isinstance(binaries, str):
requested_names = {name.strip() for name in binaries.split(",") if name.strip()}
else:
requested_names = {name for name in (binaries or ()) if name}
if not in_data_dir:
PANEL_TEXT = "\n".join(
(
@@ -231,21 +253,34 @@ def version(
subtitle="Full version info is only available when inside a collection [light_slate_blue]DATA DIR[/light_slate_blue]",
),
)
prnt()
if not in_data_dir and not requested_names:
prnt("[pale_green1][i] Binary Dependencies:[/pale_green1]")
prnt("", "[grey53]Dependency checks require an initialized collection DATA_DIR.[/grey53]")
prnt()
prnt("[deep_sky_blue3][i] Code locations:[/deep_sky_blue3]")
try:
for name, path in get_code_locations().items():
if isinstance(name, str) and isinstance(path, dict):
prnt(printable_folder_status(name, path), overflow="ignore", crop=False)
except Exception as e:
prnt(f" [red]Error getting code locations: {e}[/red]")
prnt()
prnt("[red][i] Data locations:[/red] (not in a data directory)")
prnt()
return []
prnt("[pale_green1][i] Binary Dependencies:[/pale_green1]")
failures = []
seen_failures: set[str] = set()
seen_rows: set[tuple[str, str, str, str]] = set()
from archivebox.plugins.discovery import get_enabled_plugins
from abx_dl.config import get_required_binary_requests
from abx_dl.dependencies import load_binary
from abx_dl.models import discover_plugins, filter_plugins
if isinstance(binaries, str):
requested_names = {name.strip() for name in binaries.split(",") if name.strip()}
else:
requested_names = {name for name in (binaries or ()) if name}
plugins = discover_plugins(runtime="archivebox")
enabled_plugins = filter_plugins(plugins, get_enabled_plugins(config=config), include_providers=True)
enabled_plugin_names = set(enabled_plugins)
@@ -279,7 +314,7 @@ def version(
live_enabled = console.is_terminal
live_cm = Live(_build_binary_table(rows), console=console, refresh_per_second=8) if live_enabled else None
if not live_enabled:
prnt("", "Status", "Plugin".ljust(24), "State".ljust(8), "Binary".ljust(28), "Version".ljust(16), "Provider".ljust(8), "Path")
prnt("", "Binary".ljust(28), "Status", "Plugin".ljust(24), "State".ljust(8), "Version".ljust(16), "Provider".ljust(8), "Path")
def emit_row(row: dict[str, object]) -> None:
rows.append(row)
@@ -352,8 +387,20 @@ def version(
else:
rendered_path = "[grey53]not installed[/grey53]"
status = "[red]X[/red]" if plugin_enabled else "[grey53]-[/grey53]"
if plugin_enabled:
if plugin_enabled and display_name not in seen_failures:
failures.append(display_name)
seen_failures.add(display_name)
row_key = _binary_row_dedupe_key(
display_name=display_name,
valid=valid,
version=version_str if valid else "-",
provider=provider if valid else "-",
abspath=abspath,
)
if row_key in seen_rows:
continue
seen_rows.add(row_key)
emit_row(
{
@@ -367,6 +414,50 @@ def version(
"style": "" if plugin_enabled else "dim",
},
)
if db_available:
for binary_name, installed in db_binaries.items():
if requested_names and binary_name not in requested_names:
continue
abspath = installed.abspath
version_str = (installed.version or "unknown")[:15]
provider = (installed.binprovider or "env")[:8]
row_key = _binary_row_dedupe_key(
display_name=binary_name,
valid=True,
version=version_str,
provider=provider,
abspath=abspath,
)
if row_key in seen_rows:
continue
seen_rows.add(row_key)
display_path = (
_format_binary_abspath(
abspath,
pwd=Path.cwd(),
lib_dir=config.ABXPKG_LIB_DIR,
personas_dir=CONSTANTS.PERSONAS_DIR,
home=Path.home(),
)
if compact_paths
else abspath
)
emit_row(
{
"plugin": "(database)",
"state": "detected",
"status": "[green]√[/green]",
"binary": binary_name,
"version": version_str,
"provider": provider,
"path": _render_binary_abspath(display_path) if compact_paths else display_path,
"style": "dim",
},
)
any_rows = True
any_available = True
finally:
if live_cm is not None:
live_cm.stop()
@@ -433,9 +524,7 @@ def version(
)
@docstring(version.__doc__)
def main(**kwargs):
failures = version(**kwargs)
if failures:
raise SystemExit(1)
version(**kwargs)
if __name__ == "__main__":
+1 -1
View File
@@ -664,7 +664,7 @@ class ArchiveBoxBaseConfig(
def _scoped_config(self, *, include_execution: bool) -> dict[str, Any]:
keys = type(self)._crawl_runtime_keys() if include_execution else type(self)._crawl_frozen_keys()
payload = self.model_dump(mode="json")
return {key: payload[key] for key in keys if payload.get(key) is not None}
return {key: payload[key] for key in keys if payload.get(key, None) is not None}
def for_crawl(self) -> dict[str, Any]:
"""Config scoped to crawl execution, without runtime object overlays."""
+3 -3
View File
@@ -50,9 +50,9 @@ def get_COMMIT_HASH() -> str | None:
if IN_DOCKER:
try:
version_text = Path("/VERSION.txt").read_text()
match = re.search(r"^COMMIT_HASH=([0-9a-fA-F]{40})$", version_text, re.MULTILINE)
if match:
return match.group(1)
matches = re.findall(r"^COMMIT_HASH=([0-9a-fA-F]{40})$", version_text, re.MULTILINE)
if matches:
return matches[-1]
except Exception:
pass
+9 -4
View File
@@ -145,6 +145,11 @@ ip_addrs = lambda addrs: (a for a in addrs if a.family == socket.AF_INET)
mac_addrs = lambda addrs: (a for a in addrs if a.family == psutil.AF_LINK)
def unknown_if_blank(value):
normalized = str(value or "").strip()
return normalized or "Unknown"
def get_isp_info(ip=None):
# Get public IP
try:
@@ -161,10 +166,10 @@ def get_isp_info(ip=None):
except Exception:
pass
isp = data.get("org", "Unknown")
city = data.get("city", "Unknown")
region = data.get("region", "Unknown")
country = data.get("country_name", "Unknown")
isp = unknown_if_blank(data.get("org"))
city = unknown_if_blank(data.get("city"))
region = unknown_if_blank(data.get("region"))
country = unknown_if_blank(data.get("country_name"))
# Get system DNS resolver servers
dns_server = None
@@ -0,0 +1,42 @@
# Generated by Django 6.0.6 on 2026-06-21 09:22
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("machine", "0020_repair_process_binary_iface_links"),
]
operations = [
migrations.AlterField(
model_name="networkinterface",
name="city",
field=models.CharField(default="", max_length=63),
),
migrations.AlterField(
model_name="networkinterface",
name="country",
field=models.CharField(default="", max_length=63),
),
migrations.AlterField(
model_name="networkinterface",
name="hostname",
field=models.CharField(default="", max_length=63),
),
migrations.AlterField(
model_name="networkinterface",
name="iface",
field=models.CharField(default="", max_length=15),
),
migrations.AlterField(
model_name="networkinterface",
name="isp",
field=models.CharField(default="", max_length=63),
),
migrations.AlterField(
model_name="networkinterface",
name="region",
field=models.CharField(default="", max_length=63),
),
]
@@ -0,0 +1,60 @@
# Generated by Django 6.0.6 on 2026-06-21 09:23
from django.db import migrations, models
from django.db.models import Count
def dedupe_network_interfaces_without_mac(apps, schema_editor):
NetworkInterface = apps.get_model("machine", "NetworkInterface")
Process = apps.get_model("machine", "Process")
duplicate_groups = (
NetworkInterface.objects.values(
"machine_id",
"ip_public",
"ip_local",
"dns_server",
)
.annotate(count=Count("id"))
.filter(count__gt=1)
)
for group in duplicate_groups.iterator(chunk_size=100):
lookup = {
"machine_id": group["machine_id"],
"ip_public": group["ip_public"],
"ip_local": group["ip_local"],
"dns_server": group["dns_server"],
}
keep = NetworkInterface.objects.filter(**lookup).order_by("-modified_at", "-created_at").first()
if keep is None:
continue
duplicates = NetworkInterface.objects.filter(**lookup).exclude(id=keep.id)
duplicate_ids = list(duplicates.values_list("id", flat=True))
if not duplicate_ids:
continue
Process.objects.filter(iface_id__in=duplicate_ids).update(iface_id=keep.id)
duplicates.delete()
class Migration(migrations.Migration):
dependencies = [
("machine", "0021_networkinterface_blank_defaults"),
]
operations = [
migrations.RemoveConstraint(
model_name="networkinterface",
name="unique_network_interface_identity",
),
migrations.RunPython(dedupe_network_interfaces_without_mac, migrations.RunPython.noop),
migrations.AlterUniqueTogether(
name="networkinterface",
unique_together={("machine", "ip_public", "ip_local", "dns_server")},
),
migrations.AddConstraint(
model_name="networkinterface",
constraint=models.UniqueConstraint(
fields=("machine", "ip_public", "ip_local", "dns_server"),
name="unique_network_interface_identity",
),
),
]
+8 -9
View File
@@ -394,12 +394,12 @@ class NetworkInterface(ModelWithHealthStats):
ip_public = models.GenericIPAddressField(default=None, null=False, editable=False)
ip_local = models.GenericIPAddressField(default=None, null=False, editable=False)
dns_server = models.GenericIPAddressField(default=None, null=False, editable=False)
hostname = models.CharField(max_length=63, default=None, null=False)
iface = models.CharField(max_length=15, default=None, null=False)
isp = models.CharField(max_length=63, default=None, null=False)
city = models.CharField(max_length=63, default=None, null=False)
region = models.CharField(max_length=63, default=None, null=False)
country = models.CharField(max_length=63, default=None, null=False)
hostname = models.CharField(max_length=63, default="", null=False)
iface = models.CharField(max_length=15, default="", null=False)
isp = models.CharField(max_length=63, default="", null=False)
city = models.CharField(max_length=63, default="", null=False)
region = models.CharField(max_length=63, default="", null=False)
country = models.CharField(max_length=63, default="", null=False)
# num_uses_failed = models.PositiveIntegerField(default=0) # from ModelWithHealthStats
# num_uses_succeeded = models.PositiveIntegerField(default=0) # from ModelWithHealthStats
@@ -408,10 +408,10 @@ class NetworkInterface(ModelWithHealthStats):
class Meta(ModelWithHealthStats.Meta):
app_label = "machine"
unique_together = (("machine", "ip_public", "ip_local", "mac_address", "dns_server"),)
unique_together = (("machine", "ip_public", "ip_local", "dns_server"),)
constraints = [
models.UniqueConstraint(
fields=["machine", "ip_public", "ip_local", "mac_address", "dns_server"],
fields=["machine", "ip_public", "ip_local", "dns_server"],
name="unique_network_interface_identity",
),
]
@@ -444,7 +444,6 @@ class NetworkInterface(ModelWithHealthStats):
machine=machine,
ip_public=net_info.pop("ip_public"),
ip_local=net_info.pop("ip_local"),
mac_address=net_info.pop("mac_address"),
dns_server=net_info.pop("dns_server"),
)
_CURRENT_INTERFACE = cls.objects.filter(**lookup).order_by("-modified_at", "-created_at").first()
+19 -3
View File
@@ -279,15 +279,27 @@ def run_hook(
records = process.get_records() # Get parsed JSONL output
"""
from archivebox.machine.models import Process, Machine, NetworkInterface
from archivebox.config.common import ArchiveBoxConfig, _archivebox_config_input_names, get_config, normalize_runtime_config
from archivebox.config.common import (
ArchiveBoxConfig,
_archivebox_config_input_names,
get_config,
normalize_runtime_config,
_plugin_enabled_config_keys,
)
config_scope = {key.removeprefix("config_"): kwargs.pop(key) for key in list(kwargs) if key.startswith("config_")}
config_overrides = _config_to_overrides(config)
explicit_override_keys = set(config_overrides)
resolved_config = get_config(overrides=config_overrides, **config_scope)
hook_config = normalize_runtime_config(
resolved_config.for_crawl_runtime(),
resolved_config.for_crawl_runtime(runtime_overrides=config_overrides),
json_safe=False,
)
hook_config.update(normalize_runtime_config(config_overrides, json_safe=False))
plugin_enabled_keys = set(_plugin_enabled_config_keys().values())
if plugin_enabled_keys.intersection(hook_config):
for enabled_key in plugin_enabled_keys:
hook_config.setdefault(enabled_key, False)
# Auto-detect timeout from plugin config if not explicitly provided
if timeout is None:
@@ -368,8 +380,10 @@ def run_hook(
archivebox_config_input_names = _archivebox_config_input_names()
for key in archivebox_config_input_names:
env.pop(key, None)
env.pop("PLUGINS", None)
env["DATA_DIR"] = str(CONSTANTS.DATA_DIR)
env["LIBRARY_VERSION"] = VERSION
env.setdefault("ABXPKG_FAST_SCRIPT", "1")
env.setdefault("MACHINE_ID", os.environ.get("MACHINE_ID", CONSTANTS.MACHINE_ID))
snap_dir = hook_config.get("SNAP_DIR") or _model_output_dir_from_child_path(output_dir, CONSTANTS.SNAPSHOTS_DIR_NAME)
crawl_dir = hook_config.get("CRAWL_DIR") or _model_output_dir_from_child_path(output_dir, CONSTANTS.CRAWLS_DIR_NAME)
@@ -386,7 +400,9 @@ def run_hook(
# Set Node.js module resolution paths.
# NODE_PATH may be a path list, but NODE_MODULES_DIR is a single canonical directory.
node_modules_dir = hook_config.get("NODE_MODULES_DIR")
if not node_modules_dir and lib_dir:
if lib_dir and "ABXPKG_LIB_DIR" in explicit_override_keys and "NODE_MODULES_DIR" not in explicit_override_keys:
node_modules_dir = Path(lib_dir) / "pnpm" / "packages" / "chrome" / "node_modules"
elif not node_modules_dir and lib_dir:
node_modules_dir = Path(lib_dir) / "pnpm" / "packages" / "chrome" / "node_modules"
node_path_parts = [part for part in str(hook_config.get("NODE_PATH") or "").split(os.pathsep) if part]
+26 -3
View File
@@ -31,6 +31,26 @@ def current_network_interface_with_machine():
return NetworkInterface.objects.select_related("machine").get(id=current_iface.id)
def normalize_process_env(env: dict) -> dict:
normalized = dict(env or {})
raw_plugins = normalized.pop("PLUGINS", "")
selected_plugins = {name.strip().lower() for name in str(raw_plugins).split(",") if name.strip()}
from archivebox.config.common import ArchiveBoxConfig, _archivebox_config_input_names, is_sensitive_config_key
allowed_config_keys = ArchiveBoxConfig._crawl_runtime_keys()
config_input_names = _archivebox_config_input_names()
for key in list(normalized):
if is_sensitive_config_key(key) or (key in config_input_names and key not in allowed_config_keys):
normalized.pop(key, None)
if selected_plugins:
from archivebox.config.common import _plugin_enabled_config_keys, _plugins_with_required_plugins
selected_plugins = _plugins_with_required_plugins(selected_plugins)
for plugin_name, enabled_key in _plugin_enabled_config_keys().items():
normalized.setdefault(enabled_key, "True" if plugin_name in selected_plugins else "False")
return normalized
class ProcessService(BaseService):
LISTENS_TO: ClassVar[list[type[BaseEvent]]] = [
ProcessStartedEvent,
@@ -74,6 +94,7 @@ class ProcessService(BaseService):
started_at=started_at,
)
process = await process_query.order_by("-modified_at").afirst()
process_env = normalize_process_env(event.env)
if process is None:
process = await Process.objects.acreate(
machine=iface.machine,
@@ -82,7 +103,7 @@ class ProcessService(BaseService):
worker_type=worker_type,
pwd=event.output_dir,
cmd=[event.hook_path, *event.hook_args],
env=event.env,
env=process_env,
timeout=event.timeout,
pid=event.pid or None,
url=event.url or None,
@@ -97,7 +118,7 @@ class ProcessService(BaseService):
process.pwd = event.output_dir
process.cmd = [event.hook_path, *event.hook_args]
process.env = event.env
process.env = process_env
process.timeout = event.timeout
process.pid = event.pid or None
process.url = event.url or process.url
@@ -172,6 +193,7 @@ class ProcessService(BaseService):
started_at=started_at,
)
process = await process_query.order_by("-modified_at").afirst()
process_env = normalize_process_env(event.env)
if process is None:
await Process.objects.acreate(
machine=iface.machine,
@@ -180,7 +202,7 @@ class ProcessService(BaseService):
worker_type=worker_type,
pwd=event.output_dir,
cmd=[event.hook_path, *event.hook_args],
env=event.env,
env=process_env,
timeout=event.timeout,
pid=event.pid or None,
url=event.url or None,
@@ -197,6 +219,7 @@ class ProcessService(BaseService):
"machine_id": iface.machine_id,
"iface_id": iface.id,
"pwd": event.output_dir,
"env": process_env,
"pid": event.pid or process.pid,
"url": event.url or process.url,
"process_type": process_type or process.process_type,
+20 -1
View File
@@ -55,7 +55,12 @@ from abxbus import BaseEvent
from abxbus.event_bus import EventBus, get_current_event, in_handler_context
from abxbus.event_handler import EventHandlerAbortedError, EventHandlerCancelledError
from archivebox.config.common import ArchiveBoxBaseConfig, normalize_runtime_config
from archivebox.config.common import (
ArchiveBoxBaseConfig,
normalize_runtime_config,
_plugin_enabled_config_keys,
_plugins_with_required_plugins,
)
from archivebox.misc.db import run_db_analyze_batch
from archivebox.core.shutdown_util import foreground_shutdown_signals, raise_if_shutdown_requested
from archivebox.search.sonic_daemon import register_sonic_daemon_event_handler
@@ -121,6 +126,10 @@ def _count_selected_hooks(plugins: dict[str, Plugin], selected_plugins: list[str
return sum(1 for plugin in selected.values() for hook in plugin.hooks if "CrawlSetup" in hook.name or "Snapshot" in hook.name)
def _is_nonfatal_setup_hook(plugin_name: str, hook_name: str) -> bool:
return plugin_name == "chrome" and hook_name.endswith("_chrome_kill_zombies")
def _discover_archivebox_plugins() -> dict[str, Plugin]:
return discover_plugins(runtime="archivebox")
@@ -800,6 +809,11 @@ class CrawlRunner:
},
)
normalized_config = normalize_runtime_config(config)
configured_plugins = [name.strip().lower() for name in str(normalized_config.get("PLUGINS") or "").split(",") if name.strip()]
if configured_plugins:
selected_plugin_names = _plugins_with_required_plugins(set(configured_plugins))
for plugin_name, enabled_key in _plugin_enabled_config_keys().items():
normalized_config.setdefault(enabled_key, plugin_name in selected_plugin_names)
return {
"id": str(snapshot.id),
"url": snapshot.url,
@@ -1081,6 +1095,8 @@ class CrawlRunner:
await completed_process.wait(timeout=crawl_setup_phase_timeout)
await completed_process.event_results_list()
if completed_process.status == "failed":
if _is_nonfatal_setup_hook(plugin.name, hook.name):
continue
raise RuntimeError(f"Crawl setup hook {plugin.name}:{hook.name} failed")
async def run_snapshot(self, snapshot_id: str, crawl_start_event: CrawlStartEvent | None = None) -> None:
@@ -1437,6 +1453,9 @@ def queued_plugins_for_snapshot(snapshot_id: str) -> list[str] | None:
def config_overrides_for_queued_plugins(selected_plugins: list[str], **overrides: Any) -> dict[str, Any]:
config_overrides = dict(overrides)
config_overrides["PLUGINS"] = ",".join(selected_plugins)
selected_plugin_names = _plugins_with_required_plugins({plugin_name.lower() for plugin_name in selected_plugins})
for plugin_name, enabled_key in _plugin_enabled_config_keys().items():
config_overrides[enabled_key] = plugin_name in selected_plugin_names
for plugin_name in selected_plugins:
if plugin_name.startswith("search_backend_"):
config_overrides[f"{plugin_name.upper()}_ENABLED"] = True
@@ -475,11 +475,14 @@ def test_update_index_only_runs_paused_search_rows_and_resume_later_runs_crawl(t
if snapshot_finished_before_pause:
indexed_state = get_crawl_runtime_state(tmp_path, crawl_id)
assert indexed_state["crawl_status"] == "paused"
assert indexed_state["crawl_retry_at"] == indexed_state["retry_at_max"]
assert indexed_state["snapshots"][0]["status"] == "sealed"
wget_results = [result for result in indexed_state["results"] if result["plugin"] == "wget"]
assert any(result["status"] == "succeeded" and result["output_size"] > 0 for result in wget_results)
if indexed_state["crawl_status"] == "paused":
assert indexed_state["crawl_retry_at"] == indexed_state["retry_at_max"]
else:
assert indexed_state["crawl_status"] == "sealed"
assert all(result["status"] not in {"queued", "started", "paused"} for result in indexed_state["results"])
captured_text = wait_for_snapshot_capture(tmp_path, recursive_test_site["root_url"], timeout=60)
assert "Root" in captured_text
assert "About" in captured_text
+55
View File
@@ -9,6 +9,7 @@ import re
import tempfile
from pathlib import Path
from archivebox.config.paths import tmp_dir_socket_path_is_short_enough
from archivebox.cli.archivebox_version import _binary_row_dedupe_key
from archivebox.tests.conftest import run_archivebox_cmd
@@ -30,6 +31,60 @@ def _extract_location_path(output: str, key: str) -> Path:
raise AssertionError(f"Did not find a {key} location line in output:\n{output}")
def test_binary_row_dedupe_key_keeps_distinct_paths_visible(tmp_path):
first_path = tmp_path / "lib" / "env" / "bin" / "node"
second_path = tmp_path / "other" / "bin" / "node"
first_path.parent.mkdir(parents=True)
second_path.parent.mkdir(parents=True)
first = _binary_row_dedupe_key(
display_name="node",
valid=True,
version="26.0.0",
provider="env",
abspath=str(first_path),
)
repeat = _binary_row_dedupe_key(
display_name="node",
valid=True,
version="26.0.0",
provider="env",
abspath=str(first_path),
)
different_path = _binary_row_dedupe_key(
display_name="node",
valid=True,
version="26.0.0",
provider="env",
abspath=str(second_path),
)
assert repeat == first
assert different_path != first
def test_binary_row_dedupe_key_collapses_enabled_and_disabled_plugin_references(tmp_path):
binary_path = tmp_path / "bin" / "node"
binary_path.parent.mkdir(parents=True)
enabled_reference = _binary_row_dedupe_key(
display_name="node",
valid=True,
version="26.0.0",
provider="env",
abspath=str(binary_path),
)
disabled_reference = _binary_row_dedupe_key(
display_name="node",
valid=True,
version="26.0.0",
provider="env",
abspath=str(binary_path),
)
assert disabled_reference == enabled_reference
def test_version_quiet_outputs_version_number(tmp_path):
"""Test that version --quiet outputs just the version number."""
result = run_archivebox_cmd(["version", "--quiet"])
+1 -1
View File
@@ -154,7 +154,7 @@ print(json.dumps({{
retained = json.loads(stdout.strip().splitlines()[-1])
assert retained["crawl_id"] == created["crawl_id"]
_cmd_result = run_archivebox_cmd(["run", "--crawl-id", retained["crawl_id"]], cwd=tmp_path, timeout=120, env=run_env)
_cmd_result = run_archivebox_cmd(["run", "--maintenance-only"], cwd=tmp_path, timeout=120, env=run_env)
stdout, stderr, returncode = _cmd_result.stdout, _cmd_result.stderr, _cmd_result.returncode
assert returncode == 0, f"archivebox run failed:\nSTDOUT:\n{stdout}\nSTDERR:\n{stderr}"
+42
View File
@@ -19,6 +19,7 @@ from pathlib import Path
from typing import cast
import pytest
from django.db import IntegrityError
from django.db import transaction
from django.utils import timezone
@@ -34,6 +35,7 @@ from archivebox.machine.models import (
PID_REUSE_WINDOW,
PROCESS_TIMEOUT_GRACE,
)
from archivebox.machine.detect import unknown_if_blank
pytestmark = pytest.mark.django_db
@@ -312,6 +314,46 @@ class TestNetworkInterfaceModel:
interface = NetworkInterface.current()
assert interface is not None
def test_networkinterface_optional_location_fields_default_to_blank(self, machine):
interface = NetworkInterface.objects.create(
machine=machine,
mac_address="00:00:00:00:00:01",
ip_public="127.0.0.1",
ip_local="127.0.0.1",
dns_server="127.0.0.1",
)
assert interface.hostname == ""
assert interface.iface == ""
assert interface.isp == ""
assert interface.city == ""
assert interface.region == ""
assert interface.country == ""
def test_unknown_if_blank_normalizes_null_api_fields(self):
assert unknown_if_blank(None) == "Unknown"
assert unknown_if_blank("") == "Unknown"
assert unknown_if_blank(" ") == "Unknown"
assert unknown_if_blank("California") == "California"
def test_networkinterface_identity_ignores_randomized_mac_address(self, machine):
NetworkInterface.objects.create(
machine=machine,
mac_address="00:00:00:00:00:01",
ip_public="127.0.0.1",
ip_local="127.0.0.1",
dns_server="127.0.0.1",
)
with pytest.raises(IntegrityError), transaction.atomic():
NetworkInterface.objects.create(
machine=machine,
mac_address="00:00:00:00:00:02",
ip_public="127.0.0.1",
ip_local="127.0.0.1",
dns_server="127.0.0.1",
)
class TestBinaryModel:
"""Test the Binary model."""
+2 -2
View File
@@ -590,7 +590,7 @@ class TestPublicIndexSearch:
assert response.status_code == 200
content = response.content.decode()
assert "/screenshot.png" in content
assert "screenshot/screenshot.png" not in content
assert "/screenshot/screenshot.png" not in content
@override_settings(PUBLIC_INDEX=True)
def test_public_index_preview_respects_plugin_relative_screenshot_output(self, client, public_snapshot):
@@ -634,7 +634,7 @@ class TestPublicIndexSearch:
first = content.index("chrome_extension_screenshot/screenshot-1.png")
second = content.index("chrome_extension_screenshot/screenshot.png")
assert first < second
assert "screenshot/screenshot.png" not in content
assert "/screenshot/screenshot.png" not in content
assert "chrome_extension_screenshot/screenshot-2.png" not in content
@override_settings(PUBLIC_INDEX=True)
+19
View File
@@ -34,6 +34,25 @@ def test_get_commit_hash_from_docker_version_file_ignores_short_hash(monkeypatch
assert version.get_COMMIT_HASH() == commit_hash
def test_get_commit_hash_from_docker_version_file_prefers_last_full_hash(monkeypatch) -> None:
base_hash = "a" * 40
archivebox_hash = "b" * 40
class VersionPath:
def __init__(self, path: str):
self.path = path
def read_text(self) -> str:
assert self.path == "/VERSION.txt"
return f"COMMIT_HASH={base_hash}\nCOMMIT_HASH={archivebox_hash}\n"
monkeypatch.setattr(version, "IN_DOCKER", True)
monkeypatch.setattr(version, "Path", VersionPath)
version.get_COMMIT_HASH.cache_clear()
assert version.get_COMMIT_HASH() == archivebox_hash
def test_get_commit_hash_from_detached_head(monkeypatch, tmp_path) -> None:
commit_hash = "a" * 40
package_dir = tmp_path / "archivebox"
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "archivebox",
"version": "0.9.35rc44",
"version": "0.9.35rc67",
"repository": "github:ArchiveBox/ArchiveBox",
"license": "MIT",
"dependencies": {
+1 -1
View File
@@ -1,6 +1,6 @@
[project]
name = "archivebox"
version = "0.9.35rc44"
version = "0.9.35rc67"
requires-python = ">=3.13"
description = "Self-hosted internet archiving solution."
authors = [{name = "Nick Sweeting", email = "pyproject.toml@archivebox.io"}]
Generated
+67 -62
View File
@@ -13,11 +13,11 @@ supported-markers = [
]
[options]
exclude-newer = "2026-06-14T16:57:29.927679339Z"
exclude-newer = "2026-06-16T15:03:19.333862695Z"
exclude-newer-span = "P5D"
[options.exclude-newer-package]
abxbus = { timestamp = "2026-06-19T16:57:28.927693715Z", span = "PT1S" }
abxbus = { timestamp = "2026-06-21T15:03:18.33387621Z", span = "PT1S" }
abx-plugins = "2100-01-01T00:00:00Z"
abx-dl = "2100-01-01T00:00:00Z"
abxpkg = "2100-01-01T00:00:00Z"
@@ -46,7 +46,7 @@ wheels = [
[[package]]
name = "abx-plugins"
version = "1.11.231"
version = "1.11.232"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "abxbus", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
@@ -57,9 +57,9 @@ dependencies = [
{ name = "rich-click", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
{ name = "uv", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/c5/57/f5a41cf61721955a2d01561f776271f3c0e4aaa5f875f4c4dd0725be12af/abx_plugins-1.11.231.tar.gz", hash = "sha256:7fd663d3d38f4aa71b5042625ca3aeb7e22b5ed4c03c7fed72b5848b3cd61760", size = 256596, upload-time = "2026-06-15T04:27:29.973Z" }
sdist = { url = "https://files.pythonhosted.org/packages/4e/0a/abebf52f0c449a9a3d7eabffff692b66369eb10a263d74e41e67b96f94b8/abx_plugins-1.11.232.tar.gz", hash = "sha256:dbe5772bea5f89920264716223eef376d3a1e687916550cc1bb2ebab50a118d3", size = 256700, upload-time = "2026-06-21T10:19:11.476Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/4c/04/d7293ae250420fea031ba630cea604812dff999648abda6cb3a519f6763d/abx_plugins-1.11.231-py3-none-any.whl", hash = "sha256:d868cefa0eca28b67c8f8c18f525a9e646c15fe9884560ac618c421f5dc38e74", size = 407215, upload-time = "2026-06-15T04:27:28.729Z" },
{ url = "https://files.pythonhosted.org/packages/ec/aa/7bc840e3fcb918bb06904fa9be79d168baa563eb8c6dbb034068bedca8be/abx_plugins-1.11.232-py3-none-any.whl", hash = "sha256:5e26905ad4d26f89e62bb3d3bbf4206e64f2eef7a7f0d5cb571aa5193f0d3e89", size = 407234, upload-time = "2026-06-21T10:19:09.848Z" },
]
[[package]]
@@ -78,7 +78,7 @@ wheels = [
[[package]]
name = "abxpkg"
version = "1.11.228"
version = "1.11.229"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pip", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
@@ -87,9 +87,9 @@ dependencies = [
{ name = "rich-click", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
{ name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/ac/80/8c69c1044222b88871cdca010a906f21916a30e250029835e08cd4f5b316/abxpkg-1.11.228.tar.gz", hash = "sha256:b6521d3fbdc067743d47b37c5ccffddcc6a19af74758da59b7278b9c5241426a", size = 199314, upload-time = "2026-06-15T04:04:02.191Z" }
sdist = { url = "https://files.pythonhosted.org/packages/66/30/c4728862671f1c4b1e53ec7f0d4ad111613377649ca98ed24cdf25035792/abxpkg-1.11.229.tar.gz", hash = "sha256:417ff763f2d2694b4c92b72e23dd3d9dbbd2293c6741f55225acfd05703362c1", size = 199312, upload-time = "2026-06-21T10:34:13.907Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/60/de/f2a5f52cc9389b2687bba7fbfaa73e27d3344bf16b592ada4e7c5931568e/abxpkg-1.11.228-py3-none-any.whl", hash = "sha256:d4fb57c2afe128b5b910897ca53958ea2ebeae136e6f3321d73910afa0599683", size = 215644, upload-time = "2026-06-15T04:04:00.789Z" },
{ url = "https://files.pythonhosted.org/packages/08/5d/93f900b371fb8886e5965b297229c43f0c2e2421837044037780004a8524/abxpkg-1.11.229-py3-none-any.whl", hash = "sha256:1e091c9c3f1e37f4cc7c396889c619665861b146c42c50b3f6807bfc53753f20", size = 215652, upload-time = "2026-06-21T10:34:12.38Z" },
]
[[package]]
@@ -112,19 +112,19 @@ wheels = [
[[package]]
name = "anyio"
version = "4.13.0"
version = "4.14.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "idna", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/19/14/2c5dd9f512b66549ae92767a9c7b330ae88e1932ca57876909410251fe13/anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc", size = 231622, upload-time = "2026-03-24T12:59:09.671Z" }
sdist = { url = "https://files.pythonhosted.org/packages/1c/b5/001890774a9552aff22502b8da382593109ce0c95314abaebbb116567545/anyio-4.14.0.tar.gz", hash = "sha256:b47c1f9ccf73e67021df785332508f99379c68fa7d0684e8e3492cb1d4b23f89", size = 253586, upload-time = "2026-06-15T22:00:49.021Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" },
{ url = "https://files.pythonhosted.org/packages/ba/16/9826f089383c593cdfc4a6e5aca94d9e91ae1692c57af82c3b2aa5e810f7/anyio-4.14.0-py3-none-any.whl", hash = "sha256:dd9b7a2a9799ed6552fde617b2c5df02b7fdd7d88392fc48101e51bae46164d9", size = 123506, upload-time = "2026-06-15T22:00:47.595Z" },
]
[[package]]
name = "archivebox"
version = "0.9.35rc44"
version = "0.9.35rc67"
source = { editable = "." }
dependencies = [
{ name = "abx-dl", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
@@ -715,7 +715,7 @@ wheels = [
[[package]]
name = "dateparser"
version = "1.4.0"
version = "1.4.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "python-dateutil", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
@@ -723,9 +723,9 @@ dependencies = [
{ name = "regex", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
{ name = "tzlocal", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/46/2d/a0ccdb78788064fa0dc901b8524e50615c42be1d78b78d646d0b28d09180/dateparser-1.4.0.tar.gz", hash = "sha256:97a21840d5ecdf7630c584f673338a5afac5dfe84f647baf4d7e8df98f9354a4", size = 321512, upload-time = "2026-03-26T09:56:10.292Z" }
sdist = { url = "https://files.pythonhosted.org/packages/d3/f4/561c49bca97af561d34eed27e3e831135eb5cb88e754c1150be41820f5c6/dateparser-1.4.1.tar.gz", hash = "sha256:f265df13c0380e2e07543ba74b67c0681aaa1096981ffcd35227e1aa0cb81c7c", size = 314734, upload-time = "2026-06-15T08:45:47.659Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b4/0b/3c3bb7cbe757279e693a0be6049048012f794d01f81099609ecd53b899f0/dateparser-1.4.0-py3-none-any.whl", hash = "sha256:7902b8e85d603494bf70a5a0b1decdddb2270b9c6e6b2bc8a57b93476c0df378", size = 300379, upload-time = "2026-03-26T09:56:08.409Z" },
{ url = "https://files.pythonhosted.org/packages/8b/7c/2e5dcf53909deddd0bf38cbe277ad9806be038276b1c6c436561b4d9b2e2/dateparser-1.4.1-py3-none-any.whl", hash = "sha256:f25d4e051a84be27a35bd297e3e1dc59ff78373701b89be352ba80372d22d0d0", size = 300503, upload-time = "2026-06-15T08:45:45.951Z" },
]
[[package]]
@@ -1622,23 +1622,23 @@ wheels = [
[[package]]
name = "prek"
version = "0.4.4"
version = "0.4.5"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f6/13/3d71b3adbf385f7dc7fb6e16d6e25421fd8398b45d8f8410a328bf22bd3f/prek-0.4.4.tar.gz", hash = "sha256:4ec5771153d158a0e4473933b7fd9b51e1b1f57f2df50aeb7560ea6812226dc5", size = 470641, upload-time = "2026-06-04T07:26:07.199Z" }
sdist = { url = "https://files.pythonhosted.org/packages/2f/65/23866f43521d31173879aa74bb3a2df50ab7f3f74cdb4eaa31b8f446c7ca/prek-0.4.5.tar.gz", hash = "sha256:2be7bcf839de19a0144ed5a5aadf73bc5899cf6823bb1c58cf1d45ae389c201a", size = 482566, upload-time = "2026-06-15T11:36:48.299Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/06/9f/68a577888edf7f2647a652b02899508ccd84e57ce1f79c51a44edfd308d7/prek-0.4.4-py3-none-linux_armv6l.whl", hash = "sha256:23cfd96a25de1c93e3c43c746643b80489e3b2fa49ca9c0ffd6022e51535c900", size = 5550271, upload-time = "2026-06-04T07:26:17.834Z" },
{ url = "https://files.pythonhosted.org/packages/35/b8/5427a0023116343a8d787b446536a7fddfa5db7eec7713dd05618da2bdfe/prek-0.4.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:a427b792c4436f49732b1f6ebccf221fdcc6390c148474280da9c2c6eaabc9c4", size = 5910136, upload-time = "2026-06-04T07:26:20.616Z" },
{ url = "https://files.pythonhosted.org/packages/9d/4f/d751e90b7e768e472e054cd41cbe502589436ca9c1a13bfe4fa9513f9cde/prek-0.4.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:b998038fc92c990e03147eb5b95b0f2c394517f8857ab911aac8e092f1b9b3ab", size = 5470124, upload-time = "2026-06-04T07:26:29.23Z" },
{ url = "https://files.pythonhosted.org/packages/c6/fe/e73241c5777b6f9b6b95132febbd27f9be9e89912e9e93c0982680593af2/prek-0.4.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:9cebca8c15da4f1d6e3a25e6ae0611425c8596e926222050f2588c390e42df8a", size = 5732725, upload-time = "2026-06-04T07:26:19.133Z" },
{ url = "https://files.pythonhosted.org/packages/ca/a3/329bd910e7e5d9d0eb5e571f3ba48023213744e78411afb81f5ef8356cab/prek-0.4.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c8742ac26363e74c855df6215a709d5db183204d00ac0f1a722b13aed4da3cd0", size = 5457953, upload-time = "2026-06-04T07:26:23.41Z" },
{ url = "https://files.pythonhosted.org/packages/3d/f8/d642990513d9707398506bad45d39173d84266231f7d919899f694aefe2c/prek-0.4.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a2b7c8710546a1e894afa7ab022030cd4e21f1ee7ffb301b4360773d22f1f00f", size = 5860556, upload-time = "2026-06-04T07:26:11.692Z" },
{ url = "https://files.pythonhosted.org/packages/8d/17/a2e29cb278503a8c18612d8a62a15020648dc768e2e94bc4b4d4c9411e07/prek-0.4.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e02bd4d5e05c500e4d9f70f024e30d13aa361dc490724b7f476d2e35542c239f", size = 6652492, upload-time = "2026-06-04T07:26:09.962Z" },
{ url = "https://files.pythonhosted.org/packages/ab/10/ad3270b18135ee5d1af6f6cf4b0c8601b1cc2cb38d16e835081da820833a/prek-0.4.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f3a25041733de987a47e5a7bace47182a6f0e2ae5f960cb54c1d4630afd2591", size = 6113837, upload-time = "2026-06-04T07:26:24.873Z" },
{ url = "https://files.pythonhosted.org/packages/59/d9/e8c201b9b41c4561673cea01c630ab604df89d13e952f87dbcb807d32588/prek-0.4.4-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:0b04a0f36d07474f2a9fc5b1ba1197a1b326b2b211f39cd74cf0d4613545f7f4", size = 5729155, upload-time = "2026-06-04T07:26:26.194Z" },
{ url = "https://files.pythonhosted.org/packages/e8/cd/227b0494fcbc91e8fe15c2a4db9e6dfff95314ef38db3e40e6ea96db249d/prek-0.4.4-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:f032ccbe2d6edc345f81a6d772c18cc169d63c27b5a8292bfe416b352bdfee57", size = 5590775, upload-time = "2026-06-04T07:26:22.038Z" },
{ url = "https://files.pythonhosted.org/packages/cc/e0/9d750b9cf21ece884afdc15668d0f005a36588fe1b0bb5ff4a8112ab51cb/prek-0.4.4-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:bad3586fcea3e913f0edf2e8f132f97889e03976b7ee3d120fd294ad4e89a5eb", size = 5437864, upload-time = "2026-06-04T07:26:30.673Z" },
{ url = "https://files.pythonhosted.org/packages/3a/48/e2e5c0299590ad18a253c0ac09508b615baa1b382010c511186572f711d3/prek-0.4.4-py3-none-musllinux_1_1_i686.whl", hash = "sha256:4058638532c6dcbf0076d23b9264cbdd9f0f0e320762e237a6b9e4e4b854a766", size = 5718579, upload-time = "2026-06-04T07:26:27.786Z" },
{ url = "https://files.pythonhosted.org/packages/54/48/7fb3d4e7f664d1ce8ae35ec553872ffddf9fab4c5081735fdaae610b1e7c/prek-0.4.4-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:619bab14071670249777deea0cc0b29d904c4a514cf33b20e583900a544f0399", size = 6231622, upload-time = "2026-06-04T07:26:16.309Z" },
{ url = "https://files.pythonhosted.org/packages/f4/cb/a9eedf9a35ca6ec72f12af2b4392d7f757bb24863b7b7af4523f939cf3fa/prek-0.4.5-py3-none-linux_armv6l.whl", hash = "sha256:f7517774c72b001573520dc7111156779fd3e5b4452c11f09ff53c71a067e835", size = 5618105, upload-time = "2026-06-15T11:36:21.998Z" },
{ url = "https://files.pythonhosted.org/packages/30/a7/c96c06f17db7da0a57be2be4c229aa00b525bca8001c9c765663b339cbb7/prek-0.4.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:aca9fa995536036a0171bcf7a4db96dc0a14f480054eda1d7d1c2e7739650993", size = 5972998, upload-time = "2026-06-15T11:36:41.12Z" },
{ url = "https://files.pythonhosted.org/packages/28/f1/721695355cdaa44be6f091e3a77fb9c72ed60289520f78b2f8c9a7197bdd/prek-0.4.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:66877ff21ae9d548f0f7e56fab8e65f1500a74a810e7749188c3f35a4a1b911b", size = 5525098, upload-time = "2026-06-15T11:36:30.127Z" },
{ url = "https://files.pythonhosted.org/packages/9b/1b/a334e1bb5361b49adf52b5ac7b6532018940f9f0f253437e8f43c3c1f7f3/prek-0.4.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:50697089a86a78d16f087c1912a2f3bc2bea82319a220fac52cc8e3ec9fc0426", size = 5793732, upload-time = "2026-06-15T11:36:35.745Z" },
{ url = "https://files.pythonhosted.org/packages/28/8c/aff94d276e91207a87cedff7cfefdd4aca20444137cca77bf53fffebe77a/prek-0.4.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:590427a42a3c1e5064487a0dc91167ae0c8a52168e77f574758ef9b138fcfd61", size = 5521719, upload-time = "2026-06-15T11:36:39.383Z" },
{ url = "https://files.pythonhosted.org/packages/4f/73/cfb0c5c909442050a8357e26233f7e511ba8e0d2f4b0bdc460065d62beb6/prek-0.4.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fd98b986767dafdb6b4305b563ee5a3a8f13bd3c78b98d708626815ea9f147f", size = 5922623, upload-time = "2026-06-15T11:36:18.063Z" },
{ url = "https://files.pythonhosted.org/packages/0a/ad/ff9d26551ba80d190bd08c6341176a5d56d4e6de9c2ebf077793d4adbb78/prek-0.4.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fccd11613ae92619d1ecda0ab3359ceebeb38898909ec84a8d383733d12158cc", size = 6722071, upload-time = "2026-06-15T11:36:43.086Z" },
{ url = "https://files.pythonhosted.org/packages/d3/43/11d1dfd66c919953fe89ae2fdedd4f413ee923883043816d35982177bb75/prek-0.4.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14109d37b33e5529db41a3539d4f8f72d295f6eeddede3964994d898b8cec05c", size = 6176454, upload-time = "2026-06-15T11:36:33.803Z" },
{ url = "https://files.pythonhosted.org/packages/d7/d4/9749f25c2e0ee5225f812457b888acef301e0ccce64bebcda2ac1d04abee/prek-0.4.5-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:40d262418105b2ede9836593a1927fc927cc8093c432e998640964102196996e", size = 5791133, upload-time = "2026-06-15T11:36:23.891Z" },
{ url = "https://files.pythonhosted.org/packages/c7/72/5e0344bab1eacf813a5b1b082cb4c6253930096166dad51c1cccee0a4f83/prek-0.4.5-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:a586d14c3b852fdee1c3dcd0b9cb0915db9f9d054334b854fd9470bf68edf129", size = 5658098, upload-time = "2026-06-15T11:36:44.862Z" },
{ url = "https://files.pythonhosted.org/packages/be/a5/1f406e0362dd0f18ba09a562d50d7c04a70ac05d350b1ab6fba36ca3e9f0/prek-0.4.5-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:a8ed0d28f3e7790e4402a9324c386509066df6e67cc587f7406f9a245b97b7e8", size = 5498634, upload-time = "2026-06-15T11:36:31.828Z" },
{ url = "https://files.pythonhosted.org/packages/c7/df/b0cbf0fa527330188390b7b6c8d279cd5e509923262d0a6c5cc44bbdf103/prek-0.4.5-py3-none-musllinux_1_1_i686.whl", hash = "sha256:86f76bd3d2ecf6fd9034d75c62ff4c786eb11d0dd0a1f79bbb4343b023e12769", size = 5784840, upload-time = "2026-06-15T11:36:37.481Z" },
{ url = "https://files.pythonhosted.org/packages/9d/d7/977ee3c622c906677dd94187a00392ce2dd76035486b3a3b1b5a5267dd34/prek-0.4.5-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:e491a1a4641d91d8b03dcce5588397e76d2a5b432c9b0a6c70475972b4512ab4", size = 6300384, upload-time = "2026-06-15T11:36:27.602Z" },
]
[[package]]
@@ -1899,14 +1899,13 @@ wheels = [
[[package]]
name = "pytest-codeblocks"
version = "0.17.0"
version = "0.18.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pytest", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/a5/99/1ee3017a525dcb36566f0523938fbc20fb33ef8bf957205fafe6659f3a60/pytest_codeblocks-0.17.0.tar.gz", hash = "sha256:446e1babd182f54b4f113d567737a22f5405cade144c08a0085b2985247943d5", size = 11176, upload-time = "2023-09-17T19:17:31.05Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/fa/2c/503f797c7ac1e35d81944f8fbcf3ea7a1965e435676570a833035d8d0937/pytest_codeblocks-0.17.0-py3-none-any.whl", hash = "sha256:b2aed8e66c3ce65435630783b391e7c7ae46f80b8220d3fa1bb7c689b36e78ad", size = 7716, upload-time = "2023-09-17T19:17:29.506Z" },
{ url = "https://files.pythonhosted.org/packages/3a/cb/5f40df7db75c0ac2555009287bb97b322f8d79f9f2bfdfb0bdb560834c28/pytest_codeblocks-0.18.0-py3-none-any.whl", hash = "sha256:3fe944dc505107421204c83e9232e0155eea1279b9425a10bee327079b272efb", size = 8009, upload-time = "2026-06-15T20:48:02.608Z" },
]
[[package]]
@@ -2666,11 +2665,11 @@ wheels = [
[[package]]
name = "tzlocal"
version = "5.3.1"
version = "5.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/8b/2e/c14812d3d4d9cd1773c6be938f89e5735a1f11a9f184ac3639b93cef35d5/tzlocal-5.3.1.tar.gz", hash = "sha256:cceffc7edecefea1f595541dbd6e990cb1ea3d19bf01b2809f362a03dd7921fd", size = 30761, upload-time = "2025-03-05T21:17:41.549Z" }
sdist = { url = "https://files.pythonhosted.org/packages/d8/52/ee2e6d7031687c5bad28363148cb72f2bbf38201d2e220671bd9fb830bc2/tzlocal-5.4.tar.gz", hash = "sha256:41e1293f80d4b5ff38dff222601a8fbd06b4fdcaf25e224704047ad26a39af54", size = 30922, upload-time = "2026-06-15T12:06:56.594Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c2/14/e2a54fabd4f08cd7af1c07030603c3356b74da07f7cc056e600436edfa17/tzlocal-5.3.1-py3-none-any.whl", hash = "sha256:eb1a66c3ef5847adf7a834f1be0800581b683b5608e74f86ecbcef8ab91bb85d", size = 18026, upload-time = "2025-03-05T21:17:39.857Z" },
{ url = "https://files.pythonhosted.org/packages/1d/70/5771c9ecbdb7cc0c3f3bbded7e0fa7911ee8e872ce5b5dc48ce7dce21a11/tzlocal-5.4-py3-none-any.whl", hash = "sha256:024d11221ff83453eae1f608f09b145b9779e1345d08c15404ce8ff7917cf629", size = 28261, upload-time = "2026-06-15T12:06:54.914Z" },
]
[[package]]
@@ -2693,34 +2692,40 @@ wheels = [
[[package]]
name = "ujson"
version = "5.12.1"
version = "5.13.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/bc/78/937198ea8708182dd1edbf0237bf255a96feab3f511691ad08b84da98e5d/ujson-5.12.1.tar.gz", hash = "sha256:5b7e96406c301a1366534479a7352ec40ec68bb327c0c119091635acd5925e35", size = 7164538, upload-time = "2026-05-05T22:05:01.354Z" }
sdist = { url = "https://files.pythonhosted.org/packages/89/7a/c8bb37c8f6f3623d60c33d15d18cd6d6655d0f9c3eb31a9969f76361b199/ujson-5.13.0.tar.gz", hash = "sha256:d62e3d7625384c08082abad81a077af587fdef2761bb14c3822f4234b8d07d75", size = 7166784, upload-time = "2026-06-14T22:36:50.209Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b9/f0/985b351771ebf095e2c1aaad18f4d251831226a767a32593310e4f181f19/ujson-5.12.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c4bdc052a5d097f0a2e56d93aed97355f9f7a62ef9baa4f8517e43245434af9c", size = 57959, upload-time = "2026-05-05T22:03:48.348Z" },
{ url = "https://files.pythonhosted.org/packages/61/73/03c7473372e1a538206fc655e474fa15f8bf9c46bb7c73c5fec9a544e429/ujson-5.12.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5dc91fa06ea35920b704fd9d70871897680145998071cfbf5ee3e19f2c9fc242", size = 55564, upload-time = "2026-05-05T22:03:49.869Z" },
{ url = "https://files.pythonhosted.org/packages/04/e6/104ebc35fa8dbaca66bf027c53c0c9c572271c2984576f4fd7d349d1a2e4/ujson-5.12.1-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5db0849c0e3da54822a5834f2dc51d7c51072d7f7d665014ee34600dc10889b", size = 59448, upload-time = "2026-05-05T22:03:51.224Z" },
{ url = "https://files.pythonhosted.org/packages/11/d2/55274e80fe1806cdb5cb97483be16cd6163337ab11c3bd7e28ff8a8aad26/ujson-5.12.1-cp313-cp313-manylinux_2_24_i686.manylinux_2_28_i686.whl", hash = "sha256:949cb4863a5d4847edeb47c5364b334e8cadf23a7cbdaa547d86098a4b093106", size = 61611, upload-time = "2026-05-05T22:03:52.731Z" },
{ url = "https://files.pythonhosted.org/packages/6c/15/ec46b1757c8f7770d8c101b8a463bec67c19e89c46c608d01e4b193cc64a/ujson-5.12.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8aa731138d6dfca4ab84501b72384e6c544bfb48cb87a0dd4d304df3246cac25", size = 59120, upload-time = "2026-05-05T22:03:54.064Z" },
{ url = "https://files.pythonhosted.org/packages/b5/27/ec73bc8908c33eb1f5be29d696084e531cbcfbd5c7b89ce54c025f66c682/ujson-5.12.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:727e983ef27892d86ee2d28fd517eeb02b2c1165aafcbe929dce988aeee81bfe", size = 1038913, upload-time = "2026-05-05T22:03:55.792Z" },
{ url = "https://files.pythonhosted.org/packages/6d/30/907e47569bed5f5eb258fef5e587c6759a7a062048796e40024497137e28/ujson-5.12.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d57d731ecf492d3d011e65369f8330654f0875b19f646be5270d478e843d3b81", size = 1198409, upload-time = "2026-05-05T22:03:57.947Z" },
{ url = "https://files.pythonhosted.org/packages/46/aa/f135f4b741baf14d5350be5511076408e7540353d3d850a430cb89d585a6/ujson-5.12.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1a09636220f26c66f80c6c6283023cb53120e843825f890be92696cd1aa43f39", size = 1091456, upload-time = "2026-05-05T22:04:00.355Z" },
{ url = "https://files.pythonhosted.org/packages/f8/ca/d88d86f90f8f237985f3e347b9a4f9fa24e8d30d19ec7d477ed18aa58393/ujson-5.12.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:6f19e9a407a24230df0cc1ec1c0f5999872ba526b14a780f80ad6479f5eed9bc", size = 58099, upload-time = "2026-05-05T22:04:06.688Z" },
{ url = "https://files.pythonhosted.org/packages/ae/2d/a0a88407cee3550f7ed1e49b41157ee2d410f51905ed51fb134844255280/ujson-5.12.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8b657e870c77aaacdeea86cfad3e6d2ef9b52517e45988c9c367f7ee764fe4dd", size = 55631, upload-time = "2026-05-05T22:04:07.925Z" },
{ url = "https://files.pythonhosted.org/packages/a9/6d/12a3b8e72132db244ae048075e71a0079b3c5f61ff45b7ca81d5193ab3e7/ujson-5.12.1-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:984b5a99d1e0a037c2046c3c4b34cec832565d62d5017be0a035bf3cbfab72dc", size = 59469, upload-time = "2026-05-05T22:04:09.208Z" },
{ url = "https://files.pythonhosted.org/packages/a2/72/310f8c21737554f2d2b4f1883e1a71e8a6ab0d8f92f0feb8aaa85e0f4b66/ujson-5.12.1-cp314-cp314-manylinux_2_24_i686.manylinux_2_28_i686.whl", hash = "sha256:f48ef8a16f1d85bd7982beac7adfd3fb704058631db84c1c61c8a1b7072b1508", size = 61611, upload-time = "2026-05-05T22:04:10.836Z" },
{ url = "https://files.pythonhosted.org/packages/50/50/ab4b2f7bab6c7a67298c8f2aca80e2082eaf6f332cf2d099762647b5301e/ujson-5.12.1-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f39ba3b65cc637b59731532f7e7c807786bff1d0332ab2d5b96a04d2584d78f", size = 59122, upload-time = "2026-05-05T22:04:12.137Z" },
{ url = "https://files.pythonhosted.org/packages/21/48/5d81cbe76fc2aa9e071aa489a3041cf0712f5e0663d60d501641f92b7bb4/ujson-5.12.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:07f307780f85b49cba93f291718421b6f5f3b627a323b431fad937a18f6587cb", size = 1038938, upload-time = "2026-05-05T22:04:13.548Z" },
{ url = "https://files.pythonhosted.org/packages/fb/a7/abe1acb0e5d8b8d724b35533a44c89684c88100a5fd9f2fee7f7155528d5/ujson-5.12.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:1c335caea51c31494e514b82d50763b9792d3960d2c7d9fdb6b6fb8ed50ebdd0", size = 1198416, upload-time = "2026-05-05T22:04:15.609Z" },
{ url = "https://files.pythonhosted.org/packages/ed/6e/087067d6ee22bd01bfba9fb1f32ce98c24ae2bcbab53bd2fbf8f7a80fe9e/ujson-5.12.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:19ea07e29a45d199f926aadf93a9974128438c01b83141fba32477c0ee604b33", size = 1091425, upload-time = "2026-05-05T22:04:17.909Z" },
{ url = "https://files.pythonhosted.org/packages/3f/3d/7547835cd0b7fa22eb1122702f81b2403c38a0027a2cc0d75acc449a4a66/ujson-5.12.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:49dd9c378e1c8e676785ff2b62cb490074229f15ab54abf45b623713cb2c36b5", size = 58565, upload-time = "2026-05-05T22:04:23.75Z" },
{ url = "https://files.pythonhosted.org/packages/ed/6a/1784e0b24aab50623eb47b2f7a8dc22c9d809d798854d2568a9cb7c3560f/ujson-5.12.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6d8827904358d7da59ccf2e1fd8de59e78248036d17fecc0462e62c6721f1102", size = 56157, upload-time = "2026-05-05T22:04:25.028Z" },
{ url = "https://files.pythonhosted.org/packages/91/2d/2c1b24df24eee309047d81460c3a1acf0d047207327edc6f3cab8a614985/ujson-5.12.1-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc26caebea90425662ef0b979f945f6ac832651881107d6ec9a3c4d4a4ba929c", size = 60288, upload-time = "2026-05-05T22:04:26.273Z" },
{ url = "https://files.pythonhosted.org/packages/c5/14/c0c603e3dff2ef98f7deee2df7795e6055abbc5825c6ef530024b3b06a15/ujson-5.12.1-cp314-cp314t-manylinux_2_24_i686.manylinux_2_28_i686.whl", hash = "sha256:45022aae09ac3d45bda6fbfc631088d1aff9a0465542d40bd6d295ced378c430", size = 62302, upload-time = "2026-05-05T22:04:27.516Z" },
{ url = "https://files.pythonhosted.org/packages/5c/0d/889bbc044561d9adc9bf413620fbd9878f352c9fd36da829d319bca2f5ad/ujson-5.12.1-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b22aa0f644516d3d5b29464949e4b23fe784f84b4a1030ab9ac3cb42aaedabb1", size = 59784, upload-time = "2026-05-05T22:04:28.776Z" },
{ url = "https://files.pythonhosted.org/packages/18/35/3b1d8ff8cd6dc048f5c495af6ee6ded43055562610a7e9b78b438dc6421e/ujson-5.12.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7dc5cf44ea42365cd1b66e6ed3fc6ca040c86587b024a6659b98e99d31cff2cd", size = 1039759, upload-time = "2026-05-05T22:04:30.291Z" },
{ url = "https://files.pythonhosted.org/packages/6a/d8/3c66cdf839420a6da2d6140a54a882c15efd135bcced103bd4473d577636/ujson-5.12.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:8df5d984ff4ac1ef292d70f30da03417038a7e1e0bc272d28ca9d34f02f41682", size = 1199121, upload-time = "2026-05-05T22:04:31.961Z" },
{ url = "https://files.pythonhosted.org/packages/54/51/c3d1b94a4ad27dc7532e9f7d00b869463157cede2295ba6d57566afeb8cd/ujson-5.12.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:485f0182a0c0b54c304061cdc826d8343ce595c4055f7a24e72772a8520e5f7b", size = 1092085, upload-time = "2026-05-05T22:04:33.697Z" },
{ url = "https://files.pythonhosted.org/packages/a7/f1/fe8a467d8ff5821e076b96f398d3acfe3cd568d900e6ccb41b215592b152/ujson-5.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:46998fc8d11aec34a20e2010905e7059732a3d192d9a3c3fe4f9ffd146c87ec8", size = 56746, upload-time = "2026-06-14T22:35:43.398Z" },
{ url = "https://files.pythonhosted.org/packages/6a/c0/c7ab82d6471dfa7e4fd68ae6ff2c6a50d077c05d6ecdea0cec8af635b2c4/ujson-5.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ee03ce288ba25b05cf0de87203165642277a25caa4f00a437e13152e5214e310", size = 54388, upload-time = "2026-06-14T22:35:44.586Z" },
{ url = "https://files.pythonhosted.org/packages/10/e6/4e9e998d991ff88bbc93b21daa63bba2baa61c6f952dbcec937cf7304ebe/ujson-5.13.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:cdf33b588a81b05d0b585c66f83050c49cb670623424d10e4d1ad37ba2f7eed9", size = 60051, upload-time = "2026-06-14T22:35:45.567Z" },
{ url = "https://files.pythonhosted.org/packages/9c/11/876dff43f05417a01c6119f0fa10e01f1226631c5927ef08f56876b2bb67/ujson-5.13.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4cabd73c114ce93c21d7db2e2d8e16217fd8a5b2b3ec754629eebef5c262d47f", size = 53438, upload-time = "2026-06-14T22:35:46.623Z" },
{ url = "https://files.pythonhosted.org/packages/09/02/f9dbf6c3e46d700eb1d9ed637567221a06eeb1ec289633be992ef54d7a34/ujson-5.13.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ffc61fc756a64f4d169a78cc638d769e3c324f45fc51997626abf4e5e5dd6460", size = 55060, upload-time = "2026-06-14T22:35:47.647Z" },
{ url = "https://files.pythonhosted.org/packages/bc/3d/7e49a70265a1e5ed1b5e8edd5f54d57ae41e2134faeae9b16f6f5a0eae20/ujson-5.13.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c00323c13a35822c9a67a26c0b2a0787510bf1ef490922b58009b362d1a3e21", size = 58189, upload-time = "2026-06-14T22:35:48.617Z" },
{ url = "https://files.pythonhosted.org/packages/66/34/b64278f67e19052f09810576c7e50b3da8d4f5218b226046324d4d5c24b4/ujson-5.13.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:496e662a6b46d5f936d77fb68259cece19213bb2301ddd520dbd75ac7c90c5f4", size = 57941, upload-time = "2026-06-14T22:35:49.674Z" },
{ url = "https://files.pythonhosted.org/packages/a9/8c/51513357a5c75bf3e5bae46accfdb3e6e6f5caeb72ca8b253ec45ba853fb/ujson-5.13.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7fd41b86444df14f8b4b7afaaa9f27bacfbf8c18380872317aeab6cd125dcede", size = 1037688, upload-time = "2026-06-14T22:35:50.699Z" },
{ url = "https://files.pythonhosted.org/packages/54/5a/dc6afe071d6b977390d2dc41e15800a2716f317988dd03187cffe7b4d624/ujson-5.13.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:bf3c2c4ea55d4187903fcdc689a9bf5b0fc72d8c0eaff39db18c1f337c8832c1", size = 1197141, upload-time = "2026-06-14T22:35:52.052Z" },
{ url = "https://files.pythonhosted.org/packages/50/23/b473d101412c68527cb502a8728f96ab307aa7bfa75d6ea2037e2c7f74e8/ujson-5.13.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b6eca7751d61045a9b1e7f9a8c97ac24b164f085b60bef1c4668654bb2338011", size = 1090235, upload-time = "2026-06-14T22:35:53.589Z" },
{ url = "https://files.pythonhosted.org/packages/1c/9a/b5139d696f5328f3cab70b9ec046f15e3f49497a4de6280974640602f539/ujson-5.13.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:cc9dfd41fed397ab03bb9d9fe1cbd83301211c772a17536033ce7d68877ac82b", size = 56897, upload-time = "2026-06-14T22:35:57.974Z" },
{ url = "https://files.pythonhosted.org/packages/53/55/477183aeddfdf0f88ae039ffee0ed866cfb993da0c0c9aa915807554aef8/ujson-5.13.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ca7ef2fa6c408a7c0f558e4d33d93b32ddc35ed6d3cfc505747931a64b7465d5", size = 54451, upload-time = "2026-06-14T22:35:58.932Z" },
{ url = "https://files.pythonhosted.org/packages/ea/63/55e5f23e156b4c8bca095d828b4cd3180c0b42aa3501ef88836d79606fea/ujson-5.13.0-cp314-cp314-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:a554b2e5bee85030369514cef8b0b913cebe1a4c2c0c13541966d50bcba22b1a", size = 60053, upload-time = "2026-06-14T22:35:59.969Z" },
{ url = "https://files.pythonhosted.org/packages/26/b6/08c6cf5548bd6f4bb557c9fa7e8edf87324bb04c17249d1966028d61dde0/ujson-5.13.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ea939ff629ab03ae970d03eca6d1febd8ed55ba38ca44aec64ce997537cd3fa0", size = 53481, upload-time = "2026-06-14T22:36:01.007Z" },
{ url = "https://files.pythonhosted.org/packages/6e/b3/0ac9a03551467784067f505df1bb875c639ba32f1da79ce467ab15911ada/ujson-5.13.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b98bf2faa5e37ecfe752226ea08290031e375a0c43d425a0b955fb3e702a2a71", size = 55058, upload-time = "2026-06-14T22:36:02.297Z" },
{ url = "https://files.pythonhosted.org/packages/ba/be/ec91029aec067174473d022fa0f6c3c1431a173f888d7599739f05c668eb/ujson-5.13.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9a4b92344b16e414aeb609e57f62c466500e53c94f1698f5b149dc0b7223ec3e", size = 58225, upload-time = "2026-06-14T22:36:03.321Z" },
{ url = "https://files.pythonhosted.org/packages/29/33/a948f329252ece3f9c93d177243de6e677927ebc6ac44256742dbbef3c39/ujson-5.13.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7df805aad707507a1fa165fb716218ca3a89f142125dc4b23c9fcc08fa402d97", size = 57930, upload-time = "2026-06-14T22:36:04.385Z" },
{ url = "https://files.pythonhosted.org/packages/f1/0c/c33655218b8e0a8adbf066de0b999cae5c324061f3eaa4dda17423145d9e/ujson-5.13.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7576bdbef327c3528f011002a2d74486f6fe4e33289bdb7a042b7f1a6e9d8285", size = 1037728, upload-time = "2026-06-14T22:36:05.467Z" },
{ url = "https://files.pythonhosted.org/packages/2c/bd/d286947525ea7ce3f2d8dc55c15b9ffbe425bc455c96af7b8f8a402599a9/ujson-5.13.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:6eee5d7cce3f32a468905f9ff61807a60287a90258d849460f6fa826e810870d", size = 1197146, upload-time = "2026-06-14T22:36:06.839Z" },
{ url = "https://files.pythonhosted.org/packages/3c/3c/9eb916377050b0785f048a34588c1c390ddd41ae00b78db68ee1ad022356/ujson-5.13.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:144e9d8a454cfa727e0f755e1863738ed68068583bda5463052cb446835bd56c", size = 1090223, upload-time = "2026-06-14T22:36:08.329Z" },
{ url = "https://files.pythonhosted.org/packages/12/e9/1c543837c6a3c6672361882a0fa269bd02daf9cc4c0ca88a9dccd9df98d9/ujson-5.13.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:69b4e36bb7d5f413ba8c00c8006b2ec627cc5ace97301462f6aadb66ec9d2979", size = 57402, upload-time = "2026-06-14T22:36:13.238Z" },
{ url = "https://files.pythonhosted.org/packages/99/e4/39862f0f7174ff07cfd1e2d0c9065ded34aeebdb7db8daf2f0e5bf89b46f/ujson-5.13.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8b644d50f66de5490c1823c7176618cead5e8e8a88cba9f40a6308ca52e79267", size = 54973, upload-time = "2026-06-14T22:36:14.432Z" },
{ url = "https://files.pythonhosted.org/packages/02/66/f53d3b32c3f177f846ca6b624e832f29000d8a213a2d8768e254bd470ced/ujson-5.13.0-cp314-cp314t-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:15107aaa4f559d55201165ec32abb35c283a861be1fa67229578cb7d93fcd93a", size = 60683, upload-time = "2026-06-14T22:36:15.806Z" },
{ url = "https://files.pythonhosted.org/packages/e6/d4/dddc4646d2633c85c938c2ded7d5a9711cdad5be1e13b31b7dad76f61c83/ujson-5.13.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6e343c5f0c058523f1edbf6ae4eceb4e0d934205a53bbdd8d9a945c83324662a", size = 54167, upload-time = "2026-06-14T22:36:16.952Z" },
{ url = "https://files.pythonhosted.org/packages/5f/c0/d8608c3f4d3f05e6441364b63fde1d279700135c1a6577a773662c07fbcc/ujson-5.13.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:02200035bc80e830f076ffc1b329a94c295aee6d9de8c9043647cb9a7bd4f76f", size = 55568, upload-time = "2026-06-14T22:36:17.975Z" },
{ url = "https://files.pythonhosted.org/packages/22/8e/dd12b735aaba0806c3d70c18184d50e1f9712e0757c7c0a4f376450cfe28/ujson-5.13.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d7f19b81b73ff28f5c5022ee794f94122bfcda07a76423078e349465d71223a1", size = 59086, upload-time = "2026-06-14T22:36:19.071Z" },
{ url = "https://files.pythonhosted.org/packages/48/43/ad41e8752d5ec3a590a5e7b426a54e36b7aab911d9b5a4f7384dc62507ab/ujson-5.13.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:82e1393e6dbe3c95fdfc95c6c528890e191351a1f024ef51126cf1f22543af52", size = 58667, upload-time = "2026-06-14T22:36:20.171Z" },
{ url = "https://files.pythonhosted.org/packages/e1/8e/b44a6afb77b94118655c029081b7932d64bb4c5b1c8ba2b7f5808b5d0bc2/ujson-5.13.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:38afcf994b28ed85ea2420e2a8d79a37d0a77348b3daf53850c16edda66f942d", size = 1038553, upload-time = "2026-06-14T22:36:21.245Z" },
{ url = "https://files.pythonhosted.org/packages/7e/93/fab1d786174c8780eb3e386c73f1925a435e97fbf77c957fea4fca83994d/ujson-5.13.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:1bdf2518971586f2b413156c49d9dd8b56cc990a8647081e1bd00af60564d469", size = 1197938, upload-time = "2026-06-14T22:36:22.585Z" },
{ url = "https://files.pythonhosted.org/packages/f3/bc/2f073bb708f9d128f5d1cb39063a5f6421b1ce94c61be8661c55a189f407/ujson-5.13.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:751ad01042472f1c7c02f5c597c7aee79834e82a6cc384ca302173bbc8e8deb8", size = 1090938, upload-time = "2026-06-14T22:36:23.947Z" },
]
[[package]]