Compare commits

...
Author SHA1 Message Date
ArchiveBox Release Bot 3fcda80297 Bump release version to 0.9.72rc105 2026-10-06 07:11:58 +00:00
ArchiveBox Release Bot e94a489daa Bump for abx-dl 1.13.126 2026-10-06 07:11:40 +00:00
ArchiveBox Release Bot df605452ba Bump release version to 0.9.72rc104 2026-10-06 06:42:25 +00:00
ArchiveBox Release Bot b472401d2f Bump for abx-dl 1.13.125 2026-10-06 06:42:10 +00:00
ArchiveBox Release Bot e4a1e400e8 Bump release version to 0.9.72rc103 2026-10-06 06:14:13 +00:00
Nick Sweeting 2ce7074118 Keep gallery targets in view after breakpoint changes 2026-10-05 23:13:54 -07:00
Nick Sweeting 03bd48e399 Capture complete plugin demos and expose configuration gallery views 2026-10-05 23:13:39 -07:00
ArchiveBox Release Bot edd1265b42 Bump release version to 0.9.72rc102 2026-10-05 23:19:37 +00:00
Nick Sweeting 6bafaa99d7 Verify persona browser identity across abxpkg projections 2026-10-05 16:19:14 -07:00
ArchiveBox Release Bot 53e3e9787b Bump release version to 0.9.72rc101 2026-10-05 23:08:50 +00:00
Nick Sweeting 8e82de7f4a Merge remote-tracking branch 'origin/dev' into dev 2026-10-05 16:08:21 -07:00
ArchiveBox Release Bot 41dfc384aa Bump release version to 0.9.72rc100 2026-10-05 23:08:03 +00:00
ArchiveBox Release Bot 66d88542f6 Bump for abx-dl 1.13.124 2026-10-05 23:07:45 +00:00
Nick Sweeting 268b520394 Use cached managed Chrome on hosted CI workers 2026-10-05 15:43:05 -07:00
ArchiveBox Release Bot 3a3557ac72 Bump release version to 0.9.72rc99 2026-10-05 21:24:52 +00:00
Nick Sweeting b8ba9a64cd Count shared Linux pages proportionally when admitting captures 2026-10-05 14:24:02 -07:00
ArchiveBox Release Bot 58a7955835 Bump release version to 0.9.72rc98 2026-10-05 21:05:10 +00:00
Nick Sweeting 564af2fa3e Avoid duplicate full plugin captures in retention and Docker stdin checks 2026-10-05 13:59:53 -07:00
ArchiveBox Release Bot ccaa630821 Bump release version to 0.9.72rc97 2026-10-05 02:56:19 +00:00
ArchiveBox Release Bot a3c53aca45 Bump for abx-dl 1.13.122 2026-10-05 02:56:02 +00:00
ArchiveBox Release Bot e0e3a7d09b Bump release version to 0.9.72rc96 2026-10-05 02:11:41 +00:00
Nick Sweeting c97cea180e Assert exact published image verification and plugin source ownership 2026-10-04 19:10:59 -07:00
ArchiveBox Release Bot 5efc69d04b Bump release version to 0.9.72rc95 2026-10-05 02:06:08 +00:00
Nick Sweeting 912713a732 Verify published image identities against tested digests 2026-10-04 19:05:22 -07:00
ArchiveBox Release Bot f4c84ff6ac Bump release version to 0.9.72rc94 2026-10-05 02:01:47 +00:00
Nick Sweeting 3ced9981f7 Revert "Batch measured tests under ninety seconds without changing coverage"
This reverts commit da1665acb3.
2026-10-04 19:00:23 -07:00
ArchiveBox Release Bot 49204a84e4 Bump release version to 0.9.72rc93 2026-10-05 01:49:02 +00:00
Nick Sweeting da1665acb3 Batch measured tests under ninety seconds without changing coverage 2026-10-04 18:48:16 -07:00
ArchiveBox Release Bot 5f20275598 Bump release version to 0.9.72rc92 2026-10-05 01:38:09 +00:00
ArchiveBox Release Bot 8bbd83ea3f Bump for abx-dl 1.13.121 2026-10-05 01:37:55 +00:00
ArchiveBox Release Bot 4c847dff79 Bump release version to 0.9.72rc91 2026-10-05 01:06:00 +00:00
Nick Sweeting 2e2aaf50ed Allocate distinct test service ports across workers and verify HTTP readiness 2026-10-04 18:05:38 -07:00
Nick Sweeting 9f8e58a0a0 Trace browser startup for hosted diagnosis [skip ci] 2026-10-04 17:52:20 -07:00
ArchiveBox Release Bot 6b46a328f0 Bump release version to 0.9.72rc90 2026-10-05 00:33:19 +00:00
Nick Sweeting b1cdac35d6 Import agent image and gallery behavior from the owning plugin 2026-10-04 17:33:02 -07:00
ArchiveBox Release Bot 33c4801abf Bump for abx-dl 1.13.120 2026-10-05 00:32:20 +00:00
ArchiveBox Release Bot f464d764de Bump release version to 0.9.72rc89 2026-10-04 23:58:53 +00:00
Nick Sweeting d342c38998 Fetch result process metadata without per-hook database lookups 2026-10-04 16:58:36 -07:00
Nick Sweeting f1f9a14601 Reuse loopback transport for raw replay artifact checks 2026-10-04 16:58:26 -07:00
ArchiveBox Release Bot de368a9c6a Bump release version to 0.9.72rc88 2026-10-04 23:44:29 +00:00
ArchiveBox Release Bot 0a4249b63a Bump for abx-dl 1.13.115 2026-10-04 23:43:30 +00:00
23 changed files with 303 additions and 111 deletions

No files matched your search

+6 -7
View File
@@ -274,24 +274,21 @@ jobs:
if: inputs.push_digests
run: echo ${{ steps.docker_build.outputs.digest }}
- name: Verify OpenCode install with a read-only browser profile
- name: Verify optional agent install with a read-only browser profile
shell: bash
env:
IMAGE: ${{ inputs.push_digests && format('{0}@{1}', env.DOCKERHUB_IMAGE, steps.docker_build.outputs.digest) || matrix.local_tag }}
run: |
set -Eeuo pipefail
PROFILE_DIR="$(mktemp -d)"
# shellcheck disable=SC2016
"$DOCKER_BINARY" run --rm --platform '${{ matrix.platform }}' \
--volume "$PROFILE_DIR:/home/archivebox/.config/chromium:ro" \
"$IMAGE" bash -c '
set -Eeuo pipefail
test -w "$XDG_CONFIG_HOME"
test ! -w "$XDG_CONFIG_HOME/chromium"
# Check the shipped executable before the installer can repair
# an image that accidentally omitted this opt-in dependency.
"$ABXPKG_LIB_DIR/pnpm/packages/opencode/node_modules/.bin/opencode" --version
archivebox install opencode
"$ABXPKG_LIB_DIR/pnpm/packages/opencode/node_modules/.bin/opencode" --version
uv run --no-project python -c "from abx_plugins.plugins.opencode.image import verify_archivebox_install; verify_archivebox_install()"
'
- name: Validate exact built image version and commit
@@ -454,7 +451,9 @@ jobs:
"$DOCKER_BINARY" run --name www-nginx -p 80:80 -d nginx:1.29.5@sha256:0236ee02dcbce00b9bd83e0f5fbc51069e7e1161bd59d99885b3ae1734f3392e
"$DOCKER_BINARY" run -v "$PWD"/data:/data --network host "$ARCHIVEBOX_IMAGE" add http://www.test-nginx-1.local
echo "http://www.test-nginx-2.local" | "$DOCKER_BINARY" run -i --network host -v "$PWD"/data:/data "$ARCHIVEBOX_IMAGE" add
# The argument form above exercises the complete default pipeline.
# Keep a real capture for stdin parsing without repeating every plugin.
echo "http://www.test-nginx-2.local" | "$DOCKER_BINARY" run -i --network host -v "$PWD"/data:/data "$ARCHIVEBOX_IMAGE" add --plugins=wget,hashes
archive_list="$("$DOCKER_BINARY" run -v "$PWD"/data:/data "$ARCHIVEBOX_IMAGE" list)"
[[ "$archive_list" == *"www.test-nginx-1.local"* ]] || { echo "The site 1 isn't in the list"; exit 1; }
+20 -9
View File
@@ -392,7 +392,8 @@ jobs:
- uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3
with:
install: true
# These images were already built and tested; only publish manifests.
driver: docker
- uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3
with:
@@ -525,17 +526,27 @@ jobs:
for digest in "${DIGESTS[@]}"; do REFS+=("${GHCR_IMAGE}@sha256:${digest}"); done
$DOCKER_BINARY buildx imagetools create "${TAG_ARGS[@]}" "${REFS[@]}"
- name: Verify published Docker images run
- name: Verify published tags contain the exact tested images
env:
DOCKERHUB_TAGS: ${{ steps.docker_meta.outputs.dockerhub_tags }}
GHCR_TAGS: ${{ steps.docker_meta.outputs.ghcr_tags }}
shell: bash
run: |
set -Eeuo pipefail
VERSION='${{ needs.python-release.outputs.version }}'
SHORT_SHA="${RELEASE_SHA::12}"
for image in "${DOCKERHUB_IMAGE}:sha-${SHORT_SHA}" "${GHCR_IMAGE}:sha-${SHORT_SHA}"; do
"$DOCKER_BINARY" buildx imagetools inspect "$image" >/dev/null
INSTALLED_VERSION="$("$DOCKER_BINARY" run --rm "$image" --version)"
test "$INSTALLED_VERSION" = "$VERSION"
done
# Both architectures already passed CLI/version/capture acceptance by
# digest. Verify promotion preserves those exact images and attestations
# instead of downloading and running the same image again on one arch.
mapfile -t DIGESTS < <($UV_BINARY run --no-cache --no-project python -c 'import re; from pathlib import Path; print("\n".join(sorted(path.name for path in Path("/tmp/digests").iterdir() if path.is_file() and re.fullmatch(r"[0-9a-f]{64}", path.name))))')
REFS=()
for digest in "${DIGESTS[@]}"; do REFS+=("${DOCKERHUB_IMAGE}@sha256:${digest}"); done
EXPECTED="$("$DOCKER_BINARY" buildx imagetools create --dry-run "${REFS[@]}" | "$JQ_BINARY" -ceS '.manifests | sort_by(.digest)')"
"$JQ_BINARY" -e 'map(select(.platform.os == "linux") | .platform.architecture) | sort == ["amd64", "arm64"]' <<< "$EXPECTED"
while IFS= read -r image; do
[[ -n "$image" ]] || continue
ACTUAL="$("$DOCKER_BINARY" buildx imagetools inspect --raw "$image" | "$JQ_BINARY" -ceS 'select(.schemaVersion == 2) | .manifests | sort_by(.digest)')"
[[ "$ACTUAL" == "$EXPECTED" ]] || { echo "Published tag differs from tested images: $image" >&2; exit 1; }
echo "Verified exact tested images: $image"
done < <(printf '%s\n' "$DOCKERHUB_TAGS" "$GHCR_TAGS")
- name: Record published image source
shell: bash
+3
View File
@@ -120,6 +120,9 @@ jobs:
if [[ "$CI_RUNNER_ENVIRONMENT" == github-hosted ]]; then
workers="$(nproc)"
cache_suffix="-workers-$workers"
# Avoid the hosted image's cold Chromium executable page faults.
# abxpkg owns installation and reuse in the existing runtime cache.
echo "CHROME_BINPROVIDERS=playwright,puppeteer" >> "$GITHUB_ENV"
fi
{
echo "CI_TEST_WORKERS=$workers"
+5 -7
View File
@@ -3,14 +3,14 @@
# Multistage ArchiveBox Dockerfile that consumes the abx-dl runtime image.
# WHY: abx-dl owns Python, Node, Chromium, and extraction dependencies so the
# standalone downloader stays independent of ArchiveBox's server features.
# ArchiveBox-only dependencies (Sonic, OpenCode, supervisor, Django) belong in
# ArchiveBox-only dependencies (server plugins, supervisor, Django) belong in
# the layers added by this Dockerfile, never in the shared abx-dl base image.
# REQUIRED INSTALLATION CONTRACT — do not work around failures:
# 1. IMAGE CONSTRUCTION: preinstall ALL plugins supported by runtime="archivebox"
# and ALL their required_binaries, regardless of default enabled state.
# Inherit the complete downloader dependencies from the tested abx-dl image;
# install every remaining ArchiveBox-only dependency in this image's layers,
# including OpenCode and Sonic. Never put server dependencies in abx-dl.
# including optional server plugins. Never put server dependencies in abx-dl.
# Use the normal plugin dependency pipeline: install command -> config.json
# required_binaries -> abxpkg. Never add manual apt/pip/npm installs to hide
# failures in that pipeline. OS/interpreter bootstrap is a separate concern,
@@ -266,15 +266,13 @@ RUN --mount=type=cache,target=/opt/archivebox/lib/cache,sharing=locked,mode=1777
# Install the complete ArchiveBox catalog, including disabled server plugins.
# Explicit plugin names make installation independent of enabled defaults.
RUN --mount=type=cache,target=/var/tmp/abxpkg-cache,sharing=locked,mode=1777,id=archivebox-opencode-$TARGETARCH \
RUN --mount=type=cache,target=/var/tmp/abxpkg-cache,sharing=locked,mode=1777,id=archivebox-server-plugins-$TARGETARCH \
chown -R "$DEFAULT_ARCHIVEBOX_UID:$DEFAULT_ARCHIVEBOX_GID" /var/tmp/abxpkg-cache \
&& chmod 1777 /var/tmp/abxpkg-cache \
&& export ABX_DOCKER_PLUGINS="$(/venv/bin/python3 -c 'from abx_dl.catalog import PluginCatalog; print(" ".join(PluginCatalog.discover(runtime="archivebox")))')" \
&& env ABXPKG_TMP_CACHE_DIR=/var/tmp/abxpkg-cache XDG_CACHE_HOME=/var/tmp/abxpkg-cache \
setpriv --reuid="$ARCHIVEBOX_USER" --regid="$ARCHIVEBOX_USER" --init-groups abx-dl install $ABX_DOCKER_PLUGINS \
# pnpm includes musl variants that Debian's glibc runtime cannot use.
&& find "$ABXPKG_LIB_DIR/pnpm/packages/opencode/node_modules" -type l -name 'opencode-linux-*-musl' -delete \
&& find "$ABXPKG_LIB_DIR/pnpm/packages/opencode/node_modules/.pnpm" -maxdepth 1 -type d -name 'opencode-linux-*-musl@*' -exec rm -rf {} + \
&& UV_NO_CACHE=true /usr/bin/uv run --no-project python -c 'from abx_plugins.plugins.opencode.image import prune_incompatible_image_files; prune_incompatible_image_files()' \
# pnpm's explicit store lives under ABXPKG_LIB_DIR, regardless of XDG_CACHE_HOME.
# Retaining its download copies made image size depend on whether the build
# preserved hardlinks to installed files. Remove the store in this layer;
@@ -283,7 +281,7 @@ RUN --mount=type=cache,target=/var/tmp/abxpkg-cache,sharing=locked,mode=1777,id=
&& rm -rf "$ABXPKG_LIB_DIR/cache/pnpm"
RUN --network=none export ABX_DOCKER_PLUGINS="$(/venv/bin/python3 -c 'from abx_dl.catalog import PluginCatalog; print(" ".join(PluginCatalog.discover(runtime="archivebox")))')" \
&& setpriv --reuid="$ARCHIVEBOX_USER" --regid="$ARCHIVEBOX_USER" --init-groups \
bash -c '"$ABXPKG_LIB_DIR/pnpm/packages/opencode/node_modules/.bin/opencode" --version && abx-dl install $ABX_DOCKER_PLUGINS'
bash -c 'uv run --no-project python -c "from abx_plugins.plugins.opencode.image import verify_installed; verify_installed()" && abx-dl install $ABX_DOCKER_PLUGINS'
WORKDIR "$DATA_DIR"
RUN echo "[+] Initializing image collection..." \
+22 -4
View File
@@ -211,11 +211,22 @@ class ArchiveResultFilterSchema(FilterSchema):
created_at__lt: Annotated[datetime | None, FilterLookup("created_at__lt")] = None
def _with_archiveresult_processes(queryset):
# cmd/pwd/version delegate to Process and Binary. Without this join a full
# capture's metadata response performs two extra indexed lookups per hook
# (roughly 100 queries), despite the initial Snapshot UUID lookup being fast.
# Logs and the hydrated hook environment are not response fields; loading
# those potentially large values would replace round trips with excess I/O.
return queryset.select_related("process__binary").defer("process__stdout", "process__stderr", "process__env")
@router.get("/archiveresults", response=list[ArchiveResultSchema], url_name="get_archiveresult")
@paginate(CustomPagination)
def get_archiveresults(request: HttpRequest, filters: Query[ArchiveResultFilterSchema]):
"""List all ArchiveResult entries matching these filters."""
queryset = filters.filter(ArchiveResult.objects.all())
queryset = filters.filter(
_with_archiveresult_processes(ArchiveResult.objects.all()).prefetch_related("snapshot__crawl__created_by", "snapshot__tags"),
)
if filters.search or filters.snapshot_tag:
return queryset.distinct()
return queryset
@@ -242,7 +253,12 @@ def _uuid_ref_query(field_name: str, ref: str) -> Q:
@router.get("/archiveresult/{archiveresult_id}", response=ArchiveResultSchema, url_name="get_archiveresult")
def get_archiveresult(request: HttpRequest, archiveresult_id: str):
"""Get a specific ArchiveResult by id."""
return ArchiveResult.objects.get(_uuid_ref_query("id", archiveresult_id))
return (
_with_archiveresult_processes(ArchiveResult.objects.all())
.select_related("snapshot__crawl__created_by")
.prefetch_related("snapshot__tags")
.get(_uuid_ref_query("id", archiveresult_id))
)
def _normalize_uploaded_archiveresult_plugin(plugin: str) -> str:
@@ -689,12 +705,14 @@ class SnapshotSchema(Schema):
@staticmethod
def resolve_num_archiveresults(obj, context):
return obj.archiveresult_set.all().distinct().count()
# This reverse FK contains each result once. DISTINCT would materialize
# wide JSON output columns just to count rows through snapshot_id's index.
return obj.archiveresult_set.count()
@staticmethod
def resolve_archiveresults(obj, context):
if bool(context["request"].__dict__.get("with_archiveresults", False)):
return obj.archiveresult_set.all().distinct()
return _with_archiveresult_processes(obj.archiveresult_set.all())
return ArchiveResult.objects.none()
+12
View File
@@ -170,6 +170,18 @@ class ResourceAdmission:
used_bytes = 0
for child in processes:
try:
if sys.platform.startswith("linux"):
try:
memory = child.memory_full_info()
# Chrome's processes share substantial resident
# memory. RSS charges those pages once per process
# and can falsely serialize captures on an idle host.
used_bytes += memory.pss + memory.swap
continue
except psutil.AccessDenied:
# Restricted /proc access still gets conservative
# accounting through the available RSS measurement.
pass
used_bytes += child.memory_info().rss
except (psutil.Error, OSError):
continue
+17 -3
View File
@@ -40,6 +40,7 @@ PYTEST_BASETEMP_ROOT = (REPO_ROOT / "tests" / "out").resolve()
SESSION_DATA_DIR = Path(
os.environ.get("ARCHIVEBOX_PYTEST_SESSION_DATA_DIR") or tempfile.mkdtemp(prefix="archivebox-pytest-session-"),
).resolve()
PORT_ALLOCATION_DIR = Path(os.environ.setdefault("ARCHIVEBOX_PYTEST_PORTS_DIR", str(SESSION_DATA_DIR / "ports")))
if worker_id := os.environ.get("PYTEST_XDIST_WORKER"):
# Workers inherit the coordinator's environment. Give each one its own
# collection and cleanup root so a finishing worker cannot delete another's.
@@ -1103,6 +1104,8 @@ def start_archivebox_server(
else:
output = log_path.read_text(encoding="utf-8", errors="replace") if log_path else ""
raise AssertionError(f"ArchiveBox server did not open port {port} within 30 seconds:\n{output}")
response = get_http_response(port, host=f"127.0.0.1:{port}", path="/health/", timeout=5)
assert response.status_code == 200 and response.headers.get("X-ArchiveBox-Health") == "OK", response.text
return proc
@@ -1403,9 +1406,20 @@ def init_archive(cwd: Path) -> None:
def get_free_port() -> int:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
sock.bind(("127.0.0.1", 0))
return sock.getsockname()[1]
# Binding port zero only proves availability now. Another pytest worker can
# receive that same port before either service starts. Claim it atomically
# for this session while the socket is still bound, across all workers and
# helper subprocesses; the session cleanup removes these small markers.
PORT_ALLOCATION_DIR.mkdir(parents=True, exist_ok=True)
while True:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
sock.bind(("127.0.0.1", 0))
port = sock.getsockname()[1]
try:
(PORT_ALLOCATION_DIR / str(port)).touch(exist_ok=False)
except FileExistsError:
continue
return port
def stop_server(cwd: Path) -> None:
+24 -1
View File
@@ -8,10 +8,12 @@ from urllib.parse import urlsplit
import pytest
from abx_plugins import get_plugins_dir
from django.test import Client
from django.db import connection
from django.test.utils import CaptureQueriesContext
from archivebox.core.models import ArchiveResult, Snapshot
from archivebox.core.routes_util import build_snapshot_url
from archivebox.tests.conftest import cli_env, find_snapshot_dir, parse_jsonl_output, run_archivebox_cmd
from archivebox.tests.conftest import cli_env, create_admin_and_token, find_snapshot_dir, parse_jsonl_output, run_archivebox_cmd
from archivebox.tests.test_orm_helpers import use_archivebox_db
@@ -80,6 +82,27 @@ def test_extract_runs_on_existing_snapshots(archive_with_extractors):
assert archiveresults["wget"].output_str == "wget/example.com/index.html"
assert archiveresults["wget"].output_files["example.com/index.html"]["size"] == wget_path.stat().st_size
token = create_admin_and_token(initialized_archive)
with use_archivebox_db(initialized_archive):
with CaptureQueriesContext(connection) as queries:
response = Client().get(
f"/api/v1/core/snapshot/{snapshot_id}",
HTTP_HOST="api.archivebox.localhost",
HTTP_X_ARCHIVEBOX_API_KEY=token,
)
assert response.status_code == 200, response.content
returned = {row["plugin"]: row for row in response.json()["archiveresults"]}
for plugin, result in archiveresults.items():
assert returned[plugin]["cmd"] == result.cmd
assert returned[plugin]["cmd_version"] == result.cmd_version
assert returned[plugin]["pwd"] == result.pwd
# Each real hook owns a Process and Binary. Metadata reads must fetch
# them with the result rows, not run two extra queries per plugin.
related_lookups = [
query["sql"] for query in queries if 'FROM "machine_process"' in query["sql"] or 'FROM "machine_binary"' in query["sql"]
]
assert related_lookups == []
def test_wget_literal_percent_output_url_serves_captured_file(initialized_archive, snapshot, live_server, browser_runtime):
"""Replay the real HedgeDoc link-interstitial file saved by wget."""
+4 -1
View File
@@ -52,6 +52,7 @@ def test_persona_import_real_browser_session(initialized_archive, tmp_path, http
import json
import shlex
import subprocess
from pathlib import Path
from werkzeug.wrappers import Response
from archivebox.tests.conftest import resolve_abxpkg_chrome_env
@@ -167,7 +168,9 @@ const puppeteer = require(process.argv[1]);
assert archived.returncode == 0, archived.stderr
launch_commands = list((initialized_archive / "archive" / "users").glob("*/crawls/**/chrome/cmd.sh"))
assert len(launch_commands) == 1
assert shlex.split(launch_commands[0].read_text().splitlines()[1])[0] == browser_env["CHROME_BINARY"]
launched_browser = Path(shlex.split(launch_commands[0].read_text().splitlines()[1])[0])
# abxpkg's environment and provider projections can name the same binary.
assert launched_browser.samefile(browser_env["CHROME_BINARY"])
outputs = list((initialized_archive / "archive" / "users").glob("*/snapshots/**/dom/output.html"))
assert len(outputs) == 1
html = outputs[0].read_text()
+18 -8
View File
@@ -159,7 +159,7 @@ print(json.dumps({{
assert not Path(retained["archiveresult_dir"]).exists()
def test_delete_after_real_add_page_and_rest_create_paths(client):
def test_delete_after_real_add_page_and_rest_create_paths(client, recursive_test_site):
from archivebox.personas.models import Persona
# Match init/server startup; visiting /add/ must not create persona records.
@@ -172,6 +172,7 @@ def test_delete_after_real_add_page_and_rest_create_paths(client):
)
client.force_login(admin_user)
ui_url, rest_url = recursive_test_site["child_urls"][:2]
response = client.get(reverse("add"), HTTP_HOST=ADMIN_HOST)
assert response.status_code == 200
assert 'name="delete_after"' in response.content.decode()
@@ -179,7 +180,7 @@ def test_delete_after_real_add_page_and_rest_create_paths(client):
response = client.post(
reverse("add"),
data={
"url": "https://example.com/delete-after-ui",
"url": ui_url,
"tag": "retention-ui",
"depth": "0",
"max_urls": "1",
@@ -203,13 +204,13 @@ def test_delete_after_real_add_page_and_rest_create_paths(client):
from archivebox.crawls.models import Crawl
ui_crawl = Crawl.objects.get(urls__contains="https://example.com/delete-after-ui")
ui_crawl = Crawl.objects.get(urls=ui_url)
assert ui_crawl.config["DELETE_AFTER"] == "2h"
assert ui_crawl.delete_at is not None
from archivebox.services.runner import run_due_crawl
assert run_due_crawl(ui_crawl, lock_seconds=10)
ui_snapshot = ui_crawl.snapshot_set.get(url="https://example.com/delete-after-ui")
ui_snapshot = ui_crawl.snapshot_set.get(url=ui_url)
assert ui_snapshot.delete_at is not None
assert not ui_snapshot.output_dir.joinpath("staticfile", "stdin.txt").exists()
@@ -221,10 +222,12 @@ def test_delete_after_real_add_page_and_rest_create_paths(client):
"/api/v1/crawls/crawls",
data=json.dumps(
{
"urls": ["https://example.com/delete-after-rest"],
"urls": [rest_url],
"max_depth": 0,
"max_urls": 1,
"config": {"DELETE_AFTER": "3h"},
# Exercise retention through a real capture. The full plugin
# pipeline has its own recursive-crawl integration coverage.
"config": {"DELETE_AFTER": "3h", "PLUGINS": "wget,hashes"},
},
),
content_type="application/json",
@@ -232,9 +235,16 @@ def test_delete_after_real_add_page_and_rest_create_paths(client):
HTTP_X_ARCHIVEBOX_API_KEY=api_token.token,
)
assert response.status_code == 200
rest_crawl = Crawl.objects.get(urls__contains="https://example.com/delete-after-rest")
rest_crawl = Crawl.objects.get(urls=rest_url)
assert rest_crawl.config["DELETE_AFTER"] == "3h"
assert rest_crawl.delete_at is not None
assert run_due_crawl(rest_crawl, lock_seconds=10)
rest_snapshot = rest_crawl.snapshot_set.get(url="https://example.com/delete-after-rest")
rest_snapshot = rest_crawl.snapshot_set.get(url=rest_url)
assert rest_snapshot.delete_at is not None
from archivebox.core.models import ArchiveResult
for plugin in ("wget", "hashes"):
result = rest_snapshot.archiveresult_set.get(plugin=plugin)
assert result.status == ArchiveResult.StatusChoices.SUCCEEDED
assert result.delete_at is not None
assert result.output_size > 0
+16 -5
View File
@@ -215,6 +215,8 @@ def test_release_uses_registered_publisher_and_authorized_tag_credentials():
step for step in docker_workflow["jobs"]["build"]["steps"] if step.get("name") == "Validate exact built image version and commit"
)
assert version_check["if"] == "inputs.full_tests"
assert "archivebox version" in version_check["run"]
assert "Missing exact commit marker" in version_check["run"]
pip_workflow = yaml.safe_load(PIP_WORKFLOW.read_text())
install_script = next(
step["run"] for step in pip_workflow["jobs"]["build"]["steps"] if step.get("name") == "Release wheel import and CLI smoke"
@@ -238,11 +240,20 @@ def test_release_uses_registered_publisher_and_authorized_tag_credentials():
assert 'echo "${DOCKERHUB_IMAGE}:sha-${SHORT_SHA}"' in tag_script
assert 'echo "${DOCKERHUB_IMAGE}:${VERSION}"' in tag_script
docker_verify = next(step for step in docker_release["steps"] if step.get("name") == "Verify published Docker images run")
docker_verify = next(
step for step in docker_release["steps"] if step.get("name") == "Verify published tags contain the exact tested images"
)
verify_script = docker_verify["run"]
assert '"${DOCKERHUB_IMAGE}:sha-${SHORT_SHA}"' in verify_script
assert '"${GHCR_IMAGE}:sha-${SHORT_SHA}"' in verify_script
assert '"${DOCKERHUB_IMAGE}:${VERSION}"' not in verify_script
assert docker_verify["env"] == {
"DOCKERHUB_TAGS": "${{ steps.docker_meta.outputs.dockerhub_tags }}",
"GHCR_TAGS": "${{ steps.docker_meta.outputs.ghcr_tags }}",
}
assert '"${DOCKERHUB_IMAGE}@sha256:${digest}"' in verify_script
assert 'imagetools create --dry-run "${REFS[@]}"' in verify_script
assert 'imagetools inspect --raw "$image"' in verify_script
assert 'sort == ["amd64", "arm64"]' in verify_script
assert '[[ "$ACTUAL" == "$EXPECTED" ]]' in verify_script
assert '"$DOCKERHUB_TAGS" "$GHCR_TAGS"' in verify_script
release_script = (REPO_ROOT / "bin" / "release.sh").read_text()
assert "Never create GitHub Releases for automated rc builds" in release_script
@@ -311,7 +322,7 @@ def test_stable_publication_requires_live_acceptance_before_upload():
def test_staging_acceptance_uses_published_image_source_not_workflow_run_head():
release = yaml.safe_load(RELEASE_WORKFLOW.read_text())
docker_steps = release["jobs"]["docker-release"]["steps"]
verified = next(i for i, step in enumerate(docker_steps) if step.get("name") == "Verify published Docker images run")
verified = next(i for i, step in enumerate(docker_steps) if step.get("name") == "Verify published tags contain the exact tested images")
source = next(i for i, step in enumerate(docker_steps) if step.get("name") == "Record published image source")
uploaded = next(i for i, step in enumerate(docker_steps) if step.get("name") == "Upload published image source")
assert verified < source < uploaded
@@ -1,5 +1,6 @@
import os
import subprocess
import sys
from archivebox.services.resource_admission import ResourceAdmission, disk_backed_swap_free_bytes
@@ -55,6 +56,59 @@ def test_memory_observation_tracks_a_real_child_process():
assert child.wait(timeout=10) == 0
def test_memory_observation_counts_shared_pages_once_on_linux():
admission = ResourceAdmission()
before = admission.memory_headroom()
assert before is not None
size = 128 * 1024 * 1024
child = subprocess.Popen(
[
"uv",
"run",
"python",
"-c",
"""
import os
import sys
allocation = bytearray(128 * 1024 * 1024)
children = []
for _ in range(2):
pid = os.fork()
if pid == 0:
sys.stdin.read()
os._exit(0)
children.append(pid)
print(len(allocation), flush=True)
sys.stdin.read()
for pid in children:
os.waitpid(pid, 0)
""",
],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
text=True,
)
try:
assert child.stdout is not None
assert child.stdout.readline().strip() == str(size)
during = admission.memory_headroom()
assert during is not None
growth = during[0] - before[0]
assert growth > size // 2
finite_cgroup = any(
(path / "memory.max").is_file() and (path / "memory.max").read_text().strip() != "max" for path in admission.cgroup_paths()
)
if sys.platform.startswith("linux") and not finite_cgroup:
# These three real processes share one allocation. Linux PSS and
# summing RSS differ by two copies. A finite cgroup instead measures
# the entire container, including unrelated concurrent test workers.
assert growth < size * 2, f"Shared allocation counted repeatedly: {growth}"
finally:
assert child.stdin is not None
child.stdin.close()
assert child.wait(timeout=10) == 0
def test_disk_backed_swap_headroom_excludes_zram_but_keeps_backing_swap():
swaps = """Filename Type Size Used Priority
/dev/zram0 partition 1000 100 100
@@ -11,6 +11,8 @@ from urllib.parse import urlsplit
import pytest
from playwright.sync_api import sync_playwright
from .conftest import localhost_session
@pytest.mark.django_db(transaction=True)
def test_archived_srcset_recovers_only_missing_image_candidates(snapshot, live_server, browser_runtime):
@@ -87,7 +89,9 @@ def test_archived_srcset_recovers_only_missing_image_candidates(snapshot, live_s
if density == 1:
assert "1x_web_48dp.png" in rendered.url
assert all(url.startswith((origin, "data:", "blob:")) for url in requests), requests
raw = context.request.get(f"{origin}/archivewebpage/google-drive.{archive_format}?raw=1")
assert raw.body() == recorded
with localhost_session() as session:
raw = session.get(f"{origin}/archivewebpage/google-drive.{archive_format}?raw=1")
assert raw.status_code == 200
assert raw.content == recorded
context.close()
browser.close()
+8
View File
@@ -659,6 +659,14 @@ def test_plugin_grid_includes_all_visible_plugins_and_timestamps_last(admin_clie
assert len(names) == len(expected)
for name in expected:
assert f'data-plugin-name="{name}"'.encode() in response.content
# The same schema drives both editors, including newly installed plugins.
# Check every plugin's category and rendered controls, not a fixed allowlist.
catalog = get_plugin_catalog()
for group in form.plugin_groups:
for card in group["plugins"]:
assert group["field_name"] == f"{catalog[card['name']].config.category}_plugins"
for field in card["config_fields"]:
assert f'name="{field["input_name"]}"'.encode() in response.content
postprocessing = next(group for group in form.plugin_groups if group["field_name"] == "postprocessing_plugins")
assert postprocessing["plugins"][-1]["name"] == "opentimestamps"
assert b'name="plugin_config__opentimestamps__OPENTIMESTAMPS_TIMEOUT"' in response.content
+19
View File
@@ -1,10 +1,29 @@
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from threading import Thread
from datetime import datetime, timezone
from concurrent.futures import ThreadPoolExecutor
import json
import pytest
from archivebox.misc.util import download_url, find_all_urls, fix_url_from_markdown, parse_date, sanitize_extracted_url
from archivebox.tests.conftest import get_free_port, run_python_cwd
def test_port_allocations_do_not_repeat_across_processes(tmp_path):
script = """
import json
from archivebox.tests.conftest import get_free_port
print(json.dumps([get_free_port() for _ in range(256)]))
"""
with ThreadPoolExecutor(max_workers=4) as pool:
results = list(pool.map(lambda _: run_python_cwd(script, tmp_path), range(4)))
ports = [get_free_port() for _ in range(256)]
for stdout, stderr, returncode in results:
assert returncode == 0, stderr
ports.extend(json.loads(stdout))
assert len(ports) == 1280
assert len(set(ports)) == len(ports), "Parallel services must not receive the same port"
@pytest.mark.parametrize(
+23 -31
View File
@@ -156,7 +156,6 @@ if [[ "$HAS_USEFUL_SNAPSHOT" == "0" ]]; then
--depth=0 \
--overwrite \
--tag=documentation,reference \
--plugins=title,headers,wget,screenshot,pdf,dom,readability,htmltotext,hashes,dns \
https://docs.sweeting.me/s/cookie-dilemma https://archivebox.io
)
fi
@@ -211,17 +210,10 @@ echo "[*] Creating a temporary screenshot admin"
CREATED_TEMP_USER=1
# Configure the optional UI before the server snapshots its startup config.
OPENCODE_PORT="$(uv run --no-cache --project "$REPO_DIR" python - <<'PYPORT'
import socket
with socket.socket() as sock:
sock.bind(("127.0.0.1", 0))
print(sock.getsockname()[1])
PYPORT
)"
(
cd "$DATA_DIR"
uv run --no-cache --project "$REPO_DIR" archivebox config --set "OPENCODE_PORT=$OPENCODE_PORT"
uv run --no-cache --project "$REPO_DIR" python -c \
'from abx_plugins.plugins.opencode.archivebox.screenshots import configure; configure()'
)
echo "[*] Starting ArchiveBox on port $PORT"
@@ -274,11 +266,11 @@ VIEWS=(
add_supplementary_template_capture() {
local plugin_names="$1"
local source_url="$2"
local prepare_plugins="$3"
local capture_plugins="${prepare_plugins:-$plugin_names}"
local capture_log="$CAPTURE_ROOT/${plugin_names}-supplementary-capture.log"
local supplementary_plugins=()
IFS=',' read -r -a supplementary_plugins <<<"$plugin_names"
# These are real all-plugin captures. The recipe only selects which output
# to photograph, never which extractors run or which dependencies get saved.
echo "[*] Capturing real $plugin_names source for template coverage"
if ! (
cd "$DATA_DIR"
@@ -286,7 +278,6 @@ add_supplementary_template_capture() {
--depth=0 \
--overwrite \
--tag=screenshot-gallery \
--plugins="$capture_plugins" \
"$source_url"
) >"$capture_log" 2>&1; then
echo "[!] Supplementary $plugin_names capture failed" >&2
@@ -309,20 +300,6 @@ add_supplementary_template_capture() {
local snapshot_id snapshot_view_url snapshot_view_path
IFS=$'\t' read -r snapshot_id snapshot_view_url snapshot_view_path <<<"$snapshot_record"
if [[ -n "$prepare_plugins" ]]; then
if ! (
cd "$DATA_DIR"
uv run --no-cache --project "$REPO_DIR" archivebox extract \
--plugins="$plugin_names" \
"$snapshot_id"
) >>"$capture_log" 2>&1; then
echo "[!] $plugin_names extraction failed for the prepared snapshot" >&2
tail -100 "$capture_log" >&2
exit 1
fi
stop_background_runner
fi
local discovery_report="$CAPTURE_ROOT/${plugin_names}-output-discovery.json"
NODE_PATH="$ABXPKG_LIB_DIR/pnpm/packages/chrome/node_modules" \
CHROME_BINARY="$SCREENSHOT_CHROME_BINARY" \
@@ -367,6 +344,12 @@ while [[ "$capture_index" -lt "${#VIEWS[@]}" ]]; do
[[ "$capture_mode" != wait-text:* ]] || wait_for_text="${capture_mode#wait-text:}"
preview_wait_for_text=""
[[ "$capture_mode" != wait-preview-text:* ]] || preview_wait_for_text="${capture_mode#wait-preview-text:}"
config_selector=""
config_click_selector=""
if [[ "$capture_mode" == plugin-config:* ]]; then
config_selector="[data-plugin-name='${capture_mode#plugin-config:}']"
config_click_selector="$config_selector .plugin-config-marker"
fi
view_timing_report=""
while IFS='|' read -r profile _viewport_width _viewport_height; do
filename="$slug-$profile.png"
@@ -490,6 +473,8 @@ PY
SCREENSHOT_HEIGHT=1000 \
SCREENSHOT_VARIANTS_JSON="$output_variants" \
SCREENSHOT_COLLAPSE_FILTERS=1 \
SCREENSHOT_CLICK_SELECTOR="$config_click_selector" \
SCREENSHOT_SCROLL_SELECTOR="$config_selector" \
SCREENSHOT_SNAPSHOT_HEADER="$snapshot_header" \
SCREENSHOT_WAIT_FOR_TEXT="$wait_for_text" \
SCREENSHOT_PREVIEW_WAIT_FOR_TEXT="$preview_wait_for_text" \
@@ -589,7 +574,7 @@ PY
VIEWS+=(
"Add URLs|$ADMIN_BASE_URL/add/|/add/|archivebox/core/views.py"
"Admin dashboard|$ADMIN_BASE_URL/admin/|/admin/|archivebox/core/admin_site.py"
"AI agent|$ADMIN_BASE_URL/admin/agent/|/admin/agent/|archivebox/opencode/views.py|wait-text:ArchiveBox AI Agent"
"$(uv run --no-cache --project "$REPO_DIR" python -c 'import sys; from abx_plugins.plugins.opencode.archivebox.screenshots import gallery_entry; print(gallery_entry(sys.argv[1]))' "$ADMIN_BASE_URL")"
"Snapshots table|$ADMIN_BASE_URL/admin/core/snapshot/|/admin/core/snapshot/|archivebox/core/admin_snapshots.py"
"Snapshots grid|$ADMIN_BASE_URL/admin/core/snapshot/grid/|/admin/core/snapshot/grid/|archivebox/templates/admin/snapshots_grid.html"
"Snapshot admin detail|$ADMIN_BASE_URL/admin/core/snapshot/$SNAPSHOT_ID/change/|/admin/core/snapshot/$SNAPSHOT_ID/change/|archivebox/core/admin_snapshots.py"
@@ -662,11 +647,18 @@ PY
[[ -z "$plugin_name" ]] && continue
VIEWS+=("Snapshot View ($plugin_name)|$LIVE_SNAPSHOT_VIEW_URL#$output_path|$LIVE_SNAPSHOT_VIEW_PATH|archivebox/templates/core/snapshot.html|$output_capture_mode")
done <<<"$SNAPSHOT_OUTPUT_PLUGINS"
while IFS='|' read -r example_plugins example_url example_prepare; do
while IFS='|' read -r example_plugins example_url; do
[[ -z "$example_plugins" ]] && continue
add_supplementary_template_capture "$example_plugins" "$example_url" "$example_prepare"
add_supplementary_template_capture "$example_plugins" "$example_url"
done < <(uv run --no-cache --project "$REPO_DIR" python -c \
'import json,sys; plan=json.load(open(sys.argv[1])); print("\n".join("|".join((",".join(example["plugins"]), example["url"], ",".join(example["prepare_plugins"]))) for example in plan["examples"]))' \
'import json,sys; plan=json.load(open(sys.argv[1])); print("\n".join("|".join((",".join(example["plugins"]), example["url"])) for example in plan["examples"]))' \
"$PLUGIN_SCREENSHOT_PLAN")
while IFS='|' read -r config_name config_plugin; do
[[ -z "$config_plugin" ]] && continue
VIEWS+=("$config_name|$ADMIN_BASE_URL/add/|/add/|archivebox/templates/plugins/plugin_config_grid.html|plugin-config:$config_plugin")
done < <(uv run --no-cache --project "$REPO_DIR" python -c \
'import json,sys; print("\n".join(view["name"] + "|" + view["plugin"] for view in json.load(open(sys.argv[1]))["config_views"]))' \
"$PLUGIN_SCREENSHOT_PLAN")
VIEWS+=("Snapshot View (header collapsed)|$LIVE_SNAPSHOT_VIEW_URL|$LIVE_SNAPSHOT_VIEW_PATH|archivebox/templates/core/snapshot.html|snapshot-collapsed")
+1 -1
View File
@@ -254,7 +254,7 @@ find /tmp "$TMP_DIR" -maxdepth 1 -type d -name "archivebox-chrome-profile.*" -mm
ensure_dir "/home/$ARCHIVEBOX_USER"
# A nested read-only browser-profile mount can create this parent as root.
# OpenCode's installer needs to create a sibling config directory as archivebox.
# Plugin installers need to create sibling config directories as archivebox.
ensure_dir "${XDG_CONFIG_HOME:-/home/$ARCHIVEBOX_USER/.config}"
ensure_small_runtime_tree "$ABXBUS_CACHE_DIR"
ensure_small_runtime_tree "$ABXBUS_CACHE_DIR/semaphores"
+14 -4
View File
@@ -67,6 +67,7 @@ def plugin_screenshot_plan() -> dict:
required = []
examples = {}
overrides = {}
config_views = []
enabled_plugins = []
for plugin_dir in sorted(get_plugins_dir().iterdir()):
config_path = plugin_dir / "config.json"
@@ -81,6 +82,8 @@ def plugin_screenshot_plan() -> dict:
enabled = True
if recipe.get("view"):
required.append(recipe["view"])
if recipe.get("config"):
config_views.append({"name": recipe["view"], "plugin": plugin})
elif enabled and (
recipe.get("url")
or (plugin_dir / "templates/full.html").is_file()
@@ -89,10 +92,16 @@ def plugin_screenshot_plan() -> dict:
required.append(f"Snapshot View ({plugin})")
if recipe.get("url"):
overrides[plugin] = recipe
key = (recipe["url"], tuple(recipe.get("prepare_plugins", [])))
example = examples.setdefault(key, {"url": key[0], "prepare_plugins": list(key[1]), "plugins": []})
key = recipe["url"]
example = examples.setdefault(key, {"url": key, "plugins": []})
example["plugins"].append(plugin)
return {"required": required, "examples": list(examples.values()), "overrides": overrides, "enabled": enabled_plugins}
return {
"required": required,
"examples": list(examples.values()),
"overrides": overrides,
"enabled": enabled_plugins,
"config_views": config_views,
}
def build_provenance() -> dict[str, str]:
@@ -259,7 +268,8 @@ def build_galleries(manifest_path: Path, markdown_path: Path, html_path: Path) -
route = parsed_url.path or "/"
if parsed_url.fragment:
route = f"{route}#{parsed_url.fragment}"
source_url = f"{source_base_url}{capture['source']}"
source = capture["source"]
source_url = source if source.startswith("https://") else f"{source_base_url}{source}"
ttfb_values = [variant["ttfb_ms"] for variant in variants.values() if isinstance(variant.get("ttfb_ms"), int | float)]
ttfb_text = f" · ~{round(sum(ttfb_values) / len(ttfb_values))}ms TTFB" if ttfb_values else ""
markdown_cells = []
+9 -6
View File
@@ -374,12 +374,7 @@ async function main() {
if (process.env.SCREENSHOT_AFTER_CLICK_WAIT_SELECTOR) {
await page.waitForSelector(process.env.SCREENSHOT_AFTER_CLICK_WAIT_SELECTOR, { timeout: 45000 });
}
if (process.env.SCREENSHOT_SCROLL_SELECTOR) {
await page.waitForSelector(process.env.SCREENSHOT_SCROLL_SELECTOR, { timeout: 45000 }).catch(() => {});
await page.evaluate((selector) => {
document.querySelector(selector)?.scrollIntoView({ block: 'start', inline: 'nearest' });
}, process.env.SCREENSHOT_SCROLL_SELECTOR);
}
if (requireLiveProgress) {
await waitForLiveProgress(page, liveProgressDeadline, screencastResponses);
@@ -446,6 +441,14 @@ async function main() {
const screenshotPaths = [];
if (requireLiveProgress) checks.liveProgressCaptures = [];
const capture = async (screenshotPath) => {
// Resizing can move the target to a different row/column. Re-anchor each
// gallery variant so it photographs the requested controls at every width.
if (process.env.SCREENSHOT_SCROLL_SELECTOR) {
await page.waitForSelector(process.env.SCREENSHOT_SCROLL_SELECTOR, { timeout: 45000 }).catch(() => {});
await page.evaluate((selector) => {
document.querySelector(selector)?.scrollIntoView({ block: 'start', inline: 'nearest' });
}, process.env.SCREENSHOT_SCROLL_SELECTOR);
}
await waitForVisibleCardPreviews(page, requireLiveProgress ? Math.max(1, liveProgressDeadline - Date.now()) : 45000);
if (requireLiveProgress) {
const liveProgress = await waitForLiveProgress(page, liveProgressDeadline, screencastResponses);
+1 -1
View File
@@ -56,7 +56,7 @@ View: [`/admin/`](http://admin.archivebox.localhost:5797/admin/) · [View code](
## AI agent
View: [`/admin/agent/`](http://admin.archivebox.localhost:5797/admin/agent/) · [View code](https://github.com/ArchiveBox/ArchiveBox/blob/dev/abx_plugins/plugins/opencode/views.py)
View: [`/admin/agent/`](http://admin.archivebox.localhost:5797/admin/agent/) · [View code](https://github.com/ArchiveBox/abx-plugins/blob/main/abx_plugins/plugins/opencode/archivebox/views.py)
<table><thead><tr>
<th>Desktop</th><th>Tablet</th><th>Mobile</th>
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "archivebox",
"version": "0.9.72rc87",
"version": "0.9.72rc105",
"repository": "github:ArchiveBox/ArchiveBox",
"license": "MIT",
"dependencies": {
+5 -5
View File
@@ -1,6 +1,6 @@
[project]
name = "archivebox"
version = "0.9.72rc87"
version = "0.9.72rc105"
requires-python = ">=3.13"
description = "Self-hosted internet archiving solution."
authors = [{name = "Nick Sweeting", email = "pyproject.toml@archivebox.io"}]
@@ -78,9 +78,9 @@ dependencies = [
### Extractor dependencies (runtime binaries resolved through abxpkg)
### Binary/Package Management
"abxbus==2.5.74", # direct imports only; version constrained by abx-dl -> abx-plugins
"abxpkg==1.13.28", # direct imports only; version constrained by abx-dl -> abx-plugins
"abx-plugins[opencode]==1.13.140", # optional AI runtime clients are owned by the plugin
"abx-dl==1.13.114", # shared ArchiveBox downloader package
"abxpkg==1.13.33", # direct imports only; version constrained by abx-dl -> abx-plugins
"abx-plugins[opencode]==1.13.161", # optional AI runtime clients are owned by the plugin
"abx-dl==1.13.126", # shared ArchiveBox downloader package
### UUID7 backport for Python <3.14
"uuid7>=0.1.0; python_version < '3.14'", # provides the uuid_extensions module on Python 3.13
"browser-cookie3>=0.20.1",
@@ -344,7 +344,7 @@ Donate = "https://github.com/ArchiveBox/ArchiveBox/wiki/Donations"
[tool.bumpver]
current_version = "v0.9.72rc87"
current_version = "v0.9.72rc105"
version_pattern = "vMAJOR.MINOR.PATCH[PYTAGNUM]"
commit_message = "bump version {old_version} -> {new_version}"
tag_message = "{new_version}"
Generated
+15 -15
View File
@@ -13,18 +13,18 @@ supported-markers = [
]
[options]
exclude-newer = "2026-09-29T23:03:11.928013552Z"
exclude-newer = "2026-10-01T07:11:37.418401316Z"
exclude-newer-span = "P5D"
[options.exclude-newer-package]
abxbus = { timestamp = "2026-10-04T23:03:10.928027688Z", span = "PT1S" }
abxbus = { timestamp = "2026-10-06T07:11:36.418414Z", span = "PT1S" }
abx-plugins = "2100-01-01T00:00:00Z"
abx-dl = "2100-01-01T00:00:00Z"
abxpkg = "2100-01-01T00:00:00Z"
[[package]]
name = "abx-dl"
version = "1.13.114"
version = "1.13.126"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "abx-plugins", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
@@ -38,14 +38,14 @@ dependencies = [
{ name = "rich", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
{ name = "rich-click", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/98/69/78cefa05329200e994bf6005f0aba63398381135a5a832e55feba7c3dd98/abx_dl-1.13.114.tar.gz", hash = "sha256:30158758e6a73cdc2d61b66b21d93d5081d21e53d5a15ac78a712738512a0e2b", size = 98199, upload-time = "2026-10-04T23:00:59.231Z" }
sdist = { url = "https://files.pythonhosted.org/packages/b7/80/f30434e2e081d33c1b46233e9b284c98cff9eb8895b5fd546204953849b7/abx_dl-1.13.126.tar.gz", hash = "sha256:bdd29b626fba7a35f39865e5769b3b6ea1145a9953f2cf3aa14379eae192c248", size = 98185, upload-time = "2026-10-06T07:10:19.966Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a5/78/8e4c702985b8b69436a45c8034bfcf695e6760850d58eb381ca1d28e51f2/abx_dl-1.13.114-py3-none-any.whl", hash = "sha256:e99e84d8f006c1bc9892dcbfab08e843968ea1804514cc98042039fc729b13db", size = 103548, upload-time = "2026-10-04T23:00:57.418Z" },
{ url = "https://files.pythonhosted.org/packages/ae/3e/f666f8294a31a8acd9189d790585f453e0ccecbb0a9ed9c9163c28ba6a10/abx_dl-1.13.126-py3-none-any.whl", hash = "sha256:7eb1e7295b4d11c5225d6fcd100e51069dc1022818e0c8f50abdf7d24ba4434d", size = 103547, upload-time = "2026-10-06T07:10:21.456Z" },
]
[[package]]
name = "abx-plugins"
version = "1.13.140"
version = "1.13.161"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "abxbus", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
@@ -54,9 +54,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/f5/34/4c3c74bead11db31e651121778fc4cb107615bc1d6670fd8a1c0d478ae64/abx_plugins-1.13.140.tar.gz", hash = "sha256:138e4288d7b742d5ebc16792f6f3b84e685d6af184a161dfd112bbf0e5653324", size = 487932, upload-time = "2026-10-04T22:51:51.016Z" }
sdist = { url = "https://files.pythonhosted.org/packages/3a/9d/9e442f90b30eb34fe4be44444d399685f453d3e452961018f7f91eaca173/abx_plugins-1.13.161.tar.gz", hash = "sha256:eb80af30d259716d4c713c708197328ca7fe0ad0b7fe05c27ef3bcb8112913e3", size = 509069, upload-time = "2026-10-06T07:02:15.305Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/d4/6b/197297bb156c264da4b194ec28123150ff63d7f17be015c24578d3aa712a/abx_plugins-1.13.140-py3-none-any.whl", hash = "sha256:6e7855c2f558b506911994a365d620dd040acb7cf95acee7425e493bdc018160", size = 709033, upload-time = "2026-10-04T22:51:52.653Z" },
{ url = "https://files.pythonhosted.org/packages/76/2b/da4c917bd49e92ba059b5a93b21f49499b5d385c15db23e2bc4ced834eb9/abx_plugins-1.13.161-py3-none-any.whl", hash = "sha256:1a88ec1580fa522718cf77049fd841441bd041cea63d8e38d4d1947e8b5b7b6a", size = 739583, upload-time = "2026-10-06T07:02:12.819Z" },
]
[package.optional-dependencies]
@@ -82,7 +82,7 @@ wheels = [
[[package]]
name = "abxpkg"
version = "1.13.28"
version = "1.13.33"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "platformdirs", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
@@ -90,9 +90,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/42/eb/3f802d6c788582683c5534e9d0683b871ee72b7f1d5026f6554f88890cdb/abxpkg-1.13.28.tar.gz", hash = "sha256:1daf4664c1d1645218bd52bd9356536df199a8ddd862032bab451bf6c367645d", size = 244592, upload-time = "2026-10-04T21:50:38.202Z" }
sdist = { url = "https://files.pythonhosted.org/packages/dd/6f/30d3decb924862273bf19a6674c2b8cc408424fbe05c1f5d19869226bdd4/abxpkg-1.13.33.tar.gz", hash = "sha256:ca9ecb3b37a312599573e191facf75e178394b7067e7df327acf99c32bef0c40", size = 245179, upload-time = "2026-10-05T00:16:06.849Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/3d/0c/bb90b796016dabc804ac4f06fd1e96615c681a43f7f5ccf5ab19de0bb490/abxpkg-1.13.28-py3-none-any.whl", hash = "sha256:94c0879a11ac121c2cf6cf05c783ba697dd3270af181d5b8fd7da525af06690b", size = 255914, upload-time = "2026-10-04T21:50:36.606Z" },
{ url = "https://files.pythonhosted.org/packages/25/ef/0213490bc963f473078a56f0d553deacbcef00dfa5ee80d04ca9fafb73be/abxpkg-1.13.33-py3-none-any.whl", hash = "sha256:70c089fb42a2ff06affa97fc5d2cf190b9c2a4ae9561e81e57dce3e0e2933d52", size = 256428, upload-time = "2026-10-05T00:16:05.411Z" },
]
[[package]]
@@ -127,7 +127,7 @@ wheels = [
[[package]]
name = "archivebox"
version = "0.9.72rc87"
version = "0.9.72rc105"
source = { editable = "." }
dependencies = [
{ name = "abx-dl", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
@@ -228,10 +228,10 @@ dev = [
[package.metadata]
requires-dist = [
{ name = "abx-dl", specifier = "==1.13.114" },
{ name = "abx-plugins", extras = ["opencode"], specifier = "==1.13.140" },
{ name = "abx-dl", specifier = "==1.13.126" },
{ name = "abx-plugins", extras = ["opencode"], specifier = "==1.13.161" },
{ name = "abxbus", specifier = "==2.5.74" },
{ name = "abxpkg", specifier = "==1.13.28" },
{ name = "abxpkg", specifier = "==1.13.33" },
{ name = "archivebox", extras = ["sonic", "ldap", "debug"], marker = "extra == 'all'" },
{ name = "atomicwrites", specifier = "==1.4.1" },
{ name = "base32-crockford", specifier = ">=0.3.0" },