Compare commits
66
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
60da688218 | ||
|
|
095c0e5112 | ||
|
|
f40c5629a5 | ||
|
|
4982b041c6 | ||
|
|
5679a2cfe7 | ||
|
|
711b94161a | ||
|
|
7b7d57c202 | ||
|
|
676c561372 | ||
|
|
3d2f926a82 | ||
|
|
4e21be2b1a | ||
|
|
6ac4efc864 | ||
|
|
67959c9013 | ||
|
|
50cb4f8a8d | ||
|
|
6b362fbb1a | ||
|
|
79873adc3c | ||
|
|
830422c6c6 | ||
|
|
67a8418809 | ||
|
|
75d5c53d36 | ||
|
|
8bd69a0f0e | ||
|
|
a350f58e44 | ||
|
|
c35c808da8 | ||
|
|
82724e99dc | ||
|
|
ce8a4790ae | ||
|
|
614a5bd095 | ||
|
|
215f7f5925 | ||
|
|
74399c5456 | ||
|
|
284a9fde3d | ||
|
|
fee4bf13cb | ||
|
|
be9c77320c | ||
|
|
1f0b2795bb | ||
|
|
6382722d2f | ||
|
|
4be16fc00e | ||
|
|
dcccafc00e | ||
|
|
3fa4fd5d18 | ||
|
|
ef6f616b37 | ||
|
|
0e0759a680 | ||
|
|
947c340e74 | ||
|
|
6f6eb92a90 | ||
|
|
0064e20805 | ||
|
|
59a956bf9f | ||
|
|
afd2fac50b | ||
|
|
edb6a263e9 | ||
|
|
1c00719f68 | ||
|
|
e30a4f2a1c | ||
|
|
ba198e000b | ||
|
|
24ed99a65d | ||
|
|
2cc0035a40 | ||
|
|
b41f12ee38 | ||
|
|
714caded9c | ||
|
|
200da8a167 | ||
|
|
9b02796049 | ||
|
|
062e4e12fd | ||
|
|
8d9702b423 | ||
|
|
ec728caeab | ||
|
|
7bee5417e7 | ||
|
|
580b49ab14 | ||
|
|
128e6aeff1 | ||
|
|
d8f0e0b472 | ||
|
|
76da863a02 | ||
|
|
a0d032ead3 | ||
|
|
9dbeece35b | ||
|
|
66835dcfb8 | ||
|
|
d259ac8095 | ||
|
|
00e818018a | ||
|
|
bbc12762c0 | ||
|
|
aa2df34d02 |
No files matched your search
Executable
+37
@@ -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.243"
|
||||
;;
|
||||
*)
|
||||
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"
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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
@@ -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"; \
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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__":
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -260,13 +260,15 @@ class SnapshotChangeList(SearchResultsChangeList):
|
||||
rows = (
|
||||
ArchiveResult.objects.filter(snapshot_id__in=snapshot_ids, status=ArchiveResult.StatusChoices.SUCCEEDED, output_size__gt=0)
|
||||
.order_by("snapshot_id", "plugin")
|
||||
.values_list("snapshot_id", "plugin", "status", "output_size")
|
||||
.values_list("snapshot_id", "plugin", "status", "output_size", "output_files")
|
||||
)
|
||||
for snapshot_id, plugin, status, output_size in rows.iterator(chunk_size=1000):
|
||||
for snapshot_id, plugin, status, output_size, output_files in rows.iterator(chunk_size=1000):
|
||||
if plugin in seen_plugins[snapshot_id]:
|
||||
continue
|
||||
seen_plugins[snapshot_id].add(plugin)
|
||||
results_by_snapshot[snapshot_id].append(SimpleNamespace(plugin=plugin, status=status, output_size=output_size))
|
||||
results_by_snapshot[snapshot_id].append(
|
||||
SimpleNamespace(plugin=plugin, status=status, output_size=output_size, output_files=output_files),
|
||||
)
|
||||
|
||||
for obj in self.result_list:
|
||||
obj.__dict__["_admin_archiveresults"] = results_by_snapshot[obj.pk]
|
||||
|
||||
@@ -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",
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -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()
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -162,13 +162,17 @@ def wait_for_import_processing(cwd: Path, expected_urls: set[str], *, timeout: f
|
||||
import time
|
||||
|
||||
deadline = time.time() + timeout
|
||||
counts = {url: 0 for url in expected_urls}
|
||||
while time.time() < deadline:
|
||||
with use_archivebox_db(cwd):
|
||||
snapshot_started = Snapshot.objects.filter(url__in=expected_urls).exists()
|
||||
if snapshot_started:
|
||||
rows = list(Snapshot.objects.filter(url__in=expected_urls).values_list("url", flat=True))
|
||||
counts = {url: 0 for url in expected_urls}
|
||||
for url in rows:
|
||||
counts[url] += 1
|
||||
if all(count >= 1 for count in counts.values()):
|
||||
return
|
||||
time.sleep(1)
|
||||
raise AssertionError("timed out waiting for import crawl processing to start")
|
||||
raise AssertionError(f"timed out waiting for import crawl processing to start, got counts={counts}")
|
||||
|
||||
|
||||
def wait_for_expected_import_snapshots(cwd: Path, expected_urls: set[str], *, timeout: float = 180.0) -> None:
|
||||
|
||||
@@ -228,7 +228,7 @@ SEARCH_BACKEND_PASSWORD = "SecretPassword"
|
||||
)
|
||||
|
||||
result = run_archivebox_cmd(
|
||||
["version"],
|
||||
["version", "--binaries=curl"],
|
||||
)
|
||||
|
||||
assert result.returncode == 0, result.stderr
|
||||
|
||||
@@ -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"])
|
||||
|
||||
@@ -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}"
|
||||
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -521,9 +521,7 @@ def test_recursive_crawl_depth_two_all_plugins_runs_snapshots_in_parallel(initia
|
||||
from abx_dl.models import discover_plugins
|
||||
|
||||
root_url = "https://example.com/"
|
||||
plugin_selection = ",".join(
|
||||
sorted(plugin for plugin in discover_plugins().keys() if not plugin.startswith("claude")),
|
||||
)
|
||||
plugin_selection = ",".join(sorted(plugin for plugin in discover_plugins().keys() if not plugin.startswith("claude")))
|
||||
env = os.environ.copy()
|
||||
env.pop("CHROME_BINARY", None)
|
||||
env.update(
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -516,9 +516,10 @@ def test_public_add_view_rejects_file_path_and_shell_injection_payloads(tmp_path
|
||||
with use_archivebox_db(tmp_path):
|
||||
snapshot = Snapshot.objects.get(url=safe_url)
|
||||
crawl = Crawl.objects.get()
|
||||
tag_names = set(snapshot.tags.values_list("name", flat=True))
|
||||
assert crawl.status in {Crawl.StatusChoices.STARTED, Crawl.StatusChoices.SEALED}
|
||||
assert snapshot.status in {Snapshot.StatusChoices.QUEUED, Snapshot.StatusChoices.STARTED, Snapshot.StatusChoices.SEALED}
|
||||
assert "public-ui-security" in set(snapshot.tags.values_list("name", flat=True))
|
||||
assert "public-ui-security" in tag_names
|
||||
|
||||
|
||||
@pytest.mark.timeout(180)
|
||||
@@ -580,7 +581,10 @@ def test_add_view_post_creates_schedule_over_server(tmp_path, recursive_test_sit
|
||||
|
||||
with use_archivebox_db(tmp_path):
|
||||
schedule = CrawlSchedule.objects.select_related("template").order_by("-created_at").first()
|
||||
row = (schedule.schedule, schedule.template.urls, schedule.template.tags_str) if schedule else None
|
||||
row = None
|
||||
if schedule:
|
||||
template_url = json.loads(schedule.template.urls)["url"]
|
||||
row = (schedule.schedule, template_url, schedule.template.tags_str)
|
||||
|
||||
assert row == ("daily", recursive_test_site["root_url"], "web-ui")
|
||||
finally:
|
||||
|
||||
@@ -32,6 +32,25 @@ def test_snapshot_changelist_uses_stable_ordering_without_unordered_paginator_wa
|
||||
assert b"Searching matching snapshots..." in response.content
|
||||
|
||||
|
||||
def test_snapshot_changelist_preview_uses_prefetched_output_files(admin_client, snapshot):
|
||||
from archivebox.core.models import ArchiveResult
|
||||
|
||||
ArchiveResult.objects.create(
|
||||
snapshot=snapshot,
|
||||
plugin="screenshot",
|
||||
hook_name="on_Snapshot__40_screenshot.js",
|
||||
status=ArchiveResult.StatusChoices.SUCCEEDED,
|
||||
output_size=128,
|
||||
output_files={"screenshot.png": {"size": 128, "root_relative": True}},
|
||||
)
|
||||
|
||||
response = admin_client.get(reverse("admin:core_snapshot_changelist"), HTTP_HOST=ADMIN_TEST_HOST)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert b'class="snapshot-preview' in response.content
|
||||
assert b"screenshot.png" in response.content
|
||||
|
||||
|
||||
def test_snapshot_admin_tag_editor_escapes_tag_json_script_breakout(admin_client, snapshot):
|
||||
from archivebox.core.models import Tag
|
||||
|
||||
@@ -188,10 +207,8 @@ def test_snapshot_admin_preview_uses_extension_screenshot_when_standard_screensh
|
||||
preview = admin._get_preview_data(snapshot)
|
||||
|
||||
assert preview is not None
|
||||
assert "screenshot/screenshot.png" in preview["img_url"]
|
||||
first = preview["fallback_list"].index("chrome_extension_screenshot/screenshot-1.png")
|
||||
second = preview["fallback_list"].index("chrome_extension_screenshot/screenshot.png")
|
||||
assert first < second
|
||||
assert "chrome_extension_screenshot/screenshot-1.png" in preview["img_url"]
|
||||
assert "chrome_extension_screenshot/screenshot.png" in preview["fallback_list"]
|
||||
assert "chrome_extension_screenshot/screenshot-2.png" not in preview["fallback_list"]
|
||||
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -60,6 +60,9 @@ cat > .archivebox-deploy.override.yml <<EOF
|
||||
services:
|
||||
$DEPLOY_SERVICE:
|
||||
image: $DEPLOY_IMAGE
|
||||
user: ""
|
||||
entrypoint: null
|
||||
command: null
|
||||
EOF
|
||||
|
||||
COMPOSE=(docker compose -f "$COMPOSE_FILE" -f .archivebox-deploy.override.yml)
|
||||
|
||||
@@ -27,6 +27,9 @@ cat > "$OVERRIDE_FILE" <<EOF
|
||||
services:
|
||||
$DEPLOY_SERVICE:
|
||||
image: $DEPLOY_IMAGE
|
||||
user: ""
|
||||
entrypoint: null
|
||||
command: null
|
||||
EOF
|
||||
|
||||
COMPOSE=(docker compose -f "$COMPOSE_FILE" -f "$OVERRIDE_FILE")
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "archivebox",
|
||||
"version": "0.9.35rc44",
|
||||
"version": "0.9.35rc76",
|
||||
"repository": "github:ArchiveBox/ArchiveBox",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "archivebox"
|
||||
version = "0.9.35rc44"
|
||||
version = "0.9.35rc76"
|
||||
requires-python = ">=3.13"
|
||||
description = "Self-hosted internet archiving solution."
|
||||
authors = [{name = "Nick Sweeting", email = "pyproject.toml@archivebox.io"}]
|
||||
|
||||
@@ -13,11 +13,11 @@ supported-markers = [
|
||||
]
|
||||
|
||||
[options]
|
||||
exclude-newer = "2026-06-14T16:57:29.927679339Z"
|
||||
exclude-newer = "2026-06-17T04:33:38.729143575Z"
|
||||
exclude-newer-span = "P5D"
|
||||
|
||||
[options.exclude-newer-package]
|
||||
abxbus = { timestamp = "2026-06-19T16:57:28.927693715Z", span = "PT1S" }
|
||||
abxbus = { timestamp = "2026-06-22T04:33:37.729154555Z", 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.35rc76"
|
||||
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]]
|
||||
@@ -2017,11 +2016,11 @@ sdist = { url = "https://files.pythonhosted.org/packages/b2/f4/60edeb794bbc9ed0f
|
||||
|
||||
[[package]]
|
||||
name = "python-statemachine"
|
||||
version = "3.1.2"
|
||||
version = "3.2.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/af/e2/dcc389e9855f65a3e54c859499dd409341d5ba561da74af22ce824bc4964/python_statemachine-3.1.2.tar.gz", hash = "sha256:3a64c4ae91d628eb256580f328c68b16e472f25794d2c478e91f2f1dfb8b0668", size = 710648, upload-time = "2026-05-19T00:13:12.495Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a3/88/d24d2475069c96bbd752414863d8ffc26ef25f5179c87fda5869ba8cb5d5/python_statemachine-3.2.0.tar.gz", hash = "sha256:44b98cb9bb1081891ef6efa907c821d05cae5314a2b99713e56d56a9c962de08", size = 745821, upload-time = "2026-06-17T02:25:41.347Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/f5/fe9072bfc9d245138561c09bf05b841bf6d92b4ce93d44dbfa18d96e498e/python_statemachine-3.1.2-py3-none-any.whl", hash = "sha256:89dcdbcbf7b197adade16138c75bee4f32e0c98b7eeb18d30aeb1c830dbbdd62", size = 126113, upload-time = "2026-05-19T00:13:10.854Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d8/e3/25d1076e47554b559332bba62fb4abdc01704ea960f773c8db21b4ea1952/python_statemachine-3.2.0-py3-none-any.whl", hash = "sha256:8909916fc21208680f737d33c891a5331a1a0eeeaf3fe40efa278cb056855aee", size = 152809, upload-time = "2026-06-17T02:25:39.766Z" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
@@ -2666,11 +2665,11 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "tzlocal"
|
||||
version = "5.3.1"
|
||||
version = "5.4.3"
|
||||
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/48/55/15e2340963d2bfedcc6042da3911438fd336f8ae96b65bdbe3a29766da0c/tzlocal-5.4.3.tar.gz", hash = "sha256:3a8c9bc18cf47e1dcde252ea0e6a72a6cde320a400b6ac6db1f1f8cccd553c00", size = 30873, upload-time = "2026-06-17T04:17:41.764Z" }
|
||||
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/42/28/fc144409c71569e928585f8f3c629d80d1ca3ef40175e9222f01588f98c9/tzlocal-5.4.3-py3-none-any.whl", hash = "sha256:24ce97bb58e2a973f7640ec2553ab4e6f6d5a0d0d1aa9dc43bca21d89e1feb82", size = 18039, upload-time = "2026-06-17T04:17:40.027Z" },
|
||||
]
|
||||
|
||||
[[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]]
|
||||
|
||||
Reference in new issue
Block a user