Compare commits
48
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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
@@ -67,3 +67,5 @@ Non-logged in users should be tightly limited to prevent attacks like SQL/shell
|
||||
`< v0.9.x` versions do not include any XSS/CSRF protections at all, as documented in the [readme](https://github.com/ArchiveBox/ArchiveBox/tree/v0.7.4#security-risks-of-viewing-archived-js), [wiki](https://github.com/ArchiveBox/ArchiveBox/wiki/Publishing-Your-Archive#security-concerns), [issue #239](https://github.com/ArchiveBox/ArchiveBox/issues/239), and [existing advisory](https://github.com/ArchiveBox/ArchiveBox/security/advisories/GHSA-cr45-98w9-gwqx). Do not open advisories related to XSS or CSRF for those versions, as they are missing fundamental architectural changes added in `v0.9.0` needed to support per-snapshot replay isolation.
|
||||
|
||||
[`SERVER_SECURITY_MODE`](https://github.com/archiveBox/archiveBox/wiki/configuration#server_security_mode) + CSRF/XSS defenses were only added in the new 0.9.0 `dev` work and are planned to land in `main` in mid-2026.
|
||||
|
||||
Note archivebox is designed to be able to archive any URL the server can reach, including internal intranet URLs. The only URLs that are blocked are URLs hosted by the archivebox server itself.
|
||||
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.238"
|
||||
;;
|
||||
*)
|
||||
version="$(
|
||||
python3 - "$repo_name" <<'PY'
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
repo_name = sys.argv[1]
|
||||
lock_text = Path("uv.lock").read_text()
|
||||
match = re.search(
|
||||
rf'^\[\[package\]\]\s*\nname = "{re.escape(repo_name)}"\s*\nversion = "([^"]+)"',
|
||||
lock_text,
|
||||
re.MULTILINE,
|
||||
)
|
||||
if not match:
|
||||
raise SystemExit(f"Could not find {repo_name} in uv.lock")
|
||||
print(match.group(1))
|
||||
PY
|
||||
)"
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "Cloning ArchiveBox/${repo_name}@v${version} into ${target_dir}"
|
||||
git clone --depth=1 --branch "v${version}" "https://github.com/ArchiveBox/${repo_name}.git" "$target_dir"
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -34,6 +34,25 @@ def test_get_commit_hash_from_docker_version_file_ignores_short_hash(monkeypatch
|
||||
assert version.get_COMMIT_HASH() == commit_hash
|
||||
|
||||
|
||||
def test_get_commit_hash_from_docker_version_file_prefers_last_full_hash(monkeypatch) -> None:
|
||||
base_hash = "a" * 40
|
||||
archivebox_hash = "b" * 40
|
||||
|
||||
class VersionPath:
|
||||
def __init__(self, path: str):
|
||||
self.path = path
|
||||
|
||||
def read_text(self) -> str:
|
||||
assert self.path == "/VERSION.txt"
|
||||
return f"COMMIT_HASH={base_hash}\nCOMMIT_HASH={archivebox_hash}\n"
|
||||
|
||||
monkeypatch.setattr(version, "IN_DOCKER", True)
|
||||
monkeypatch.setattr(version, "Path", VersionPath)
|
||||
version.get_COMMIT_HASH.cache_clear()
|
||||
|
||||
assert version.get_COMMIT_HASH() == archivebox_hash
|
||||
|
||||
|
||||
def test_get_commit_hash_from_detached_head(monkeypatch, tmp_path) -> None:
|
||||
commit_hash = "a" * 40
|
||||
package_dir = tmp_path / "archivebox"
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "archivebox",
|
||||
"version": "0.9.35rc44",
|
||||
"version": "0.9.35rc67",
|
||||
"repository": "github:ArchiveBox/ArchiveBox",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "archivebox"
|
||||
version = "0.9.35rc44"
|
||||
version = "0.9.35rc67"
|
||||
requires-python = ">=3.13"
|
||||
description = "Self-hosted internet archiving solution."
|
||||
authors = [{name = "Nick Sweeting", email = "pyproject.toml@archivebox.io"}]
|
||||
|
||||
@@ -13,11 +13,11 @@ supported-markers = [
|
||||
]
|
||||
|
||||
[options]
|
||||
exclude-newer = "2026-06-14T16:57:29.927679339Z"
|
||||
exclude-newer = "2026-06-16T15:03:19.333862695Z"
|
||||
exclude-newer-span = "P5D"
|
||||
|
||||
[options.exclude-newer-package]
|
||||
abxbus = { timestamp = "2026-06-19T16:57:28.927693715Z", span = "PT1S" }
|
||||
abxbus = { timestamp = "2026-06-21T15:03:18.33387621Z", span = "PT1S" }
|
||||
abx-plugins = "2100-01-01T00:00:00Z"
|
||||
abx-dl = "2100-01-01T00:00:00Z"
|
||||
abxpkg = "2100-01-01T00:00:00Z"
|
||||
@@ -46,7 +46,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "abx-plugins"
|
||||
version = "1.11.231"
|
||||
version = "1.11.232"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "abxbus", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||
@@ -57,9 +57,9 @@ dependencies = [
|
||||
{ name = "rich-click", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||
{ name = "uv", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c5/57/f5a41cf61721955a2d01561f776271f3c0e4aaa5f875f4c4dd0725be12af/abx_plugins-1.11.231.tar.gz", hash = "sha256:7fd663d3d38f4aa71b5042625ca3aeb7e22b5ed4c03c7fed72b5848b3cd61760", size = 256596, upload-time = "2026-06-15T04:27:29.973Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/4e/0a/abebf52f0c449a9a3d7eabffff692b66369eb10a263d74e41e67b96f94b8/abx_plugins-1.11.232.tar.gz", hash = "sha256:dbe5772bea5f89920264716223eef376d3a1e687916550cc1bb2ebab50a118d3", size = 256700, upload-time = "2026-06-21T10:19:11.476Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/4c/04/d7293ae250420fea031ba630cea604812dff999648abda6cb3a519f6763d/abx_plugins-1.11.231-py3-none-any.whl", hash = "sha256:d868cefa0eca28b67c8f8c18f525a9e646c15fe9884560ac618c421f5dc38e74", size = 407215, upload-time = "2026-06-15T04:27:28.729Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ec/aa/7bc840e3fcb918bb06904fa9be79d168baa563eb8c6dbb034068bedca8be/abx_plugins-1.11.232-py3-none-any.whl", hash = "sha256:5e26905ad4d26f89e62bb3d3bbf4206e64f2eef7a7f0d5cb571aa5193f0d3e89", size = 407234, upload-time = "2026-06-21T10:19:09.848Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -78,7 +78,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "abxpkg"
|
||||
version = "1.11.228"
|
||||
version = "1.11.229"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pip", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||
@@ -87,9 +87,9 @@ dependencies = [
|
||||
{ name = "rich-click", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||
{ name = "typing-extensions", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/ac/80/8c69c1044222b88871cdca010a906f21916a30e250029835e08cd4f5b316/abxpkg-1.11.228.tar.gz", hash = "sha256:b6521d3fbdc067743d47b37c5ccffddcc6a19af74758da59b7278b9c5241426a", size = 199314, upload-time = "2026-06-15T04:04:02.191Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/66/30/c4728862671f1c4b1e53ec7f0d4ad111613377649ca98ed24cdf25035792/abxpkg-1.11.229.tar.gz", hash = "sha256:417ff763f2d2694b4c92b72e23dd3d9dbbd2293c6741f55225acfd05703362c1", size = 199312, upload-time = "2026-06-21T10:34:13.907Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/60/de/f2a5f52cc9389b2687bba7fbfaa73e27d3344bf16b592ada4e7c5931568e/abxpkg-1.11.228-py3-none-any.whl", hash = "sha256:d4fb57c2afe128b5b910897ca53958ea2ebeae136e6f3321d73910afa0599683", size = 215644, upload-time = "2026-06-15T04:04:00.789Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/08/5d/93f900b371fb8886e5965b297229c43f0c2e2421837044037780004a8524/abxpkg-1.11.229-py3-none-any.whl", hash = "sha256:1e091c9c3f1e37f4cc7c396889c619665861b146c42c50b3f6807bfc53753f20", size = 215652, upload-time = "2026-06-21T10:34:12.38Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -112,19 +112,19 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "anyio"
|
||||
version = "4.13.0"
|
||||
version = "4.14.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "idna", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/19/14/2c5dd9f512b66549ae92767a9c7b330ae88e1932ca57876909410251fe13/anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc", size = 231622, upload-time = "2026-03-24T12:59:09.671Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/1c/b5/001890774a9552aff22502b8da382593109ce0c95314abaebbb116567545/anyio-4.14.0.tar.gz", hash = "sha256:b47c1f9ccf73e67021df785332508f99379c68fa7d0684e8e3492cb1d4b23f89", size = 253586, upload-time = "2026-06-15T22:00:49.021Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/16/9826f089383c593cdfc4a6e5aca94d9e91ae1692c57af82c3b2aa5e810f7/anyio-4.14.0-py3-none-any.whl", hash = "sha256:dd9b7a2a9799ed6552fde617b2c5df02b7fdd7d88392fc48101e51bae46164d9", size = 123506, upload-time = "2026-06-15T22:00:47.595Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "archivebox"
|
||||
version = "0.9.35rc44"
|
||||
version = "0.9.35rc67"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "abx-dl", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||
@@ -715,7 +715,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "dateparser"
|
||||
version = "1.4.0"
|
||||
version = "1.4.1"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "python-dateutil", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||
@@ -723,9 +723,9 @@ dependencies = [
|
||||
{ name = "regex", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||
{ name = "tzlocal", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/46/2d/a0ccdb78788064fa0dc901b8524e50615c42be1d78b78d646d0b28d09180/dateparser-1.4.0.tar.gz", hash = "sha256:97a21840d5ecdf7630c584f673338a5afac5dfe84f647baf4d7e8df98f9354a4", size = 321512, upload-time = "2026-03-26T09:56:10.292Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d3/f4/561c49bca97af561d34eed27e3e831135eb5cb88e754c1150be41820f5c6/dateparser-1.4.1.tar.gz", hash = "sha256:f265df13c0380e2e07543ba74b67c0681aaa1096981ffcd35227e1aa0cb81c7c", size = 314734, upload-time = "2026-06-15T08:45:47.659Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b4/0b/3c3bb7cbe757279e693a0be6049048012f794d01f81099609ecd53b899f0/dateparser-1.4.0-py3-none-any.whl", hash = "sha256:7902b8e85d603494bf70a5a0b1decdddb2270b9c6e6b2bc8a57b93476c0df378", size = 300379, upload-time = "2026-03-26T09:56:08.409Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8b/7c/2e5dcf53909deddd0bf38cbe277ad9806be038276b1c6c436561b4d9b2e2/dateparser-1.4.1-py3-none-any.whl", hash = "sha256:f25d4e051a84be27a35bd297e3e1dc59ff78373701b89be352ba80372d22d0d0", size = 300503, upload-time = "2026-06-15T08:45:45.951Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1622,23 +1622,23 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "prek"
|
||||
version = "0.4.4"
|
||||
version = "0.4.5"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/f6/13/3d71b3adbf385f7dc7fb6e16d6e25421fd8398b45d8f8410a328bf22bd3f/prek-0.4.4.tar.gz", hash = "sha256:4ec5771153d158a0e4473933b7fd9b51e1b1f57f2df50aeb7560ea6812226dc5", size = 470641, upload-time = "2026-06-04T07:26:07.199Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/2f/65/23866f43521d31173879aa74bb3a2df50ab7f3f74cdb4eaa31b8f446c7ca/prek-0.4.5.tar.gz", hash = "sha256:2be7bcf839de19a0144ed5a5aadf73bc5899cf6823bb1c58cf1d45ae389c201a", size = 482566, upload-time = "2026-06-15T11:36:48.299Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/06/9f/68a577888edf7f2647a652b02899508ccd84e57ce1f79c51a44edfd308d7/prek-0.4.4-py3-none-linux_armv6l.whl", hash = "sha256:23cfd96a25de1c93e3c43c746643b80489e3b2fa49ca9c0ffd6022e51535c900", size = 5550271, upload-time = "2026-06-04T07:26:17.834Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/35/b8/5427a0023116343a8d787b446536a7fddfa5db7eec7713dd05618da2bdfe/prek-0.4.4-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:a427b792c4436f49732b1f6ebccf221fdcc6390c148474280da9c2c6eaabc9c4", size = 5910136, upload-time = "2026-06-04T07:26:20.616Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9d/4f/d751e90b7e768e472e054cd41cbe502589436ca9c1a13bfe4fa9513f9cde/prek-0.4.4-py3-none-macosx_11_0_arm64.whl", hash = "sha256:b998038fc92c990e03147eb5b95b0f2c394517f8857ab911aac8e092f1b9b3ab", size = 5470124, upload-time = "2026-06-04T07:26:29.23Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c6/fe/e73241c5777b6f9b6b95132febbd27f9be9e89912e9e93c0982680593af2/prek-0.4.4-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:9cebca8c15da4f1d6e3a25e6ae0611425c8596e926222050f2588c390e42df8a", size = 5732725, upload-time = "2026-06-04T07:26:19.133Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ca/a3/329bd910e7e5d9d0eb5e571f3ba48023213744e78411afb81f5ef8356cab/prek-0.4.4-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c8742ac26363e74c855df6215a709d5db183204d00ac0f1a722b13aed4da3cd0", size = 5457953, upload-time = "2026-06-04T07:26:23.41Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3d/f8/d642990513d9707398506bad45d39173d84266231f7d919899f694aefe2c/prek-0.4.4-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a2b7c8710546a1e894afa7ab022030cd4e21f1ee7ffb301b4360773d22f1f00f", size = 5860556, upload-time = "2026-06-04T07:26:11.692Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/8d/17/a2e29cb278503a8c18612d8a62a15020648dc768e2e94bc4b4d4c9411e07/prek-0.4.4-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e02bd4d5e05c500e4d9f70f024e30d13aa361dc490724b7f476d2e35542c239f", size = 6652492, upload-time = "2026-06-04T07:26:09.962Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ab/10/ad3270b18135ee5d1af6f6cf4b0c8601b1cc2cb38d16e835081da820833a/prek-0.4.4-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3f3a25041733de987a47e5a7bace47182a6f0e2ae5f960cb54c1d4630afd2591", size = 6113837, upload-time = "2026-06-04T07:26:24.873Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/59/d9/e8c201b9b41c4561673cea01c630ab604df89d13e952f87dbcb807d32588/prek-0.4.4-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:0b04a0f36d07474f2a9fc5b1ba1197a1b326b2b211f39cd74cf0d4613545f7f4", size = 5729155, upload-time = "2026-06-04T07:26:26.194Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e8/cd/227b0494fcbc91e8fe15c2a4db9e6dfff95314ef38db3e40e6ea96db249d/prek-0.4.4-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:f032ccbe2d6edc345f81a6d772c18cc169d63c27b5a8292bfe416b352bdfee57", size = 5590775, upload-time = "2026-06-04T07:26:22.038Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cc/e0/9d750b9cf21ece884afdc15668d0f005a36588fe1b0bb5ff4a8112ab51cb/prek-0.4.4-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:bad3586fcea3e913f0edf2e8f132f97889e03976b7ee3d120fd294ad4e89a5eb", size = 5437864, upload-time = "2026-06-04T07:26:30.673Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3a/48/e2e5c0299590ad18a253c0ac09508b615baa1b382010c511186572f711d3/prek-0.4.4-py3-none-musllinux_1_1_i686.whl", hash = "sha256:4058638532c6dcbf0076d23b9264cbdd9f0f0e320762e237a6b9e4e4b854a766", size = 5718579, upload-time = "2026-06-04T07:26:27.786Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/54/48/7fb3d4e7f664d1ce8ae35ec553872ffddf9fab4c5081735fdaae610b1e7c/prek-0.4.4-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:619bab14071670249777deea0cc0b29d904c4a514cf33b20e583900a544f0399", size = 6231622, upload-time = "2026-06-04T07:26:16.309Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f4/cb/a9eedf9a35ca6ec72f12af2b4392d7f757bb24863b7b7af4523f939cf3fa/prek-0.4.5-py3-none-linux_armv6l.whl", hash = "sha256:f7517774c72b001573520dc7111156779fd3e5b4452c11f09ff53c71a067e835", size = 5618105, upload-time = "2026-06-15T11:36:21.998Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/30/a7/c96c06f17db7da0a57be2be4c229aa00b525bca8001c9c765663b339cbb7/prek-0.4.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:aca9fa995536036a0171bcf7a4db96dc0a14f480054eda1d7d1c2e7739650993", size = 5972998, upload-time = "2026-06-15T11:36:41.12Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/28/f1/721695355cdaa44be6f091e3a77fb9c72ed60289520f78b2f8c9a7197bdd/prek-0.4.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:66877ff21ae9d548f0f7e56fab8e65f1500a74a810e7749188c3f35a4a1b911b", size = 5525098, upload-time = "2026-06-15T11:36:30.127Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9b/1b/a334e1bb5361b49adf52b5ac7b6532018940f9f0f253437e8f43c3c1f7f3/prek-0.4.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:50697089a86a78d16f087c1912a2f3bc2bea82319a220fac52cc8e3ec9fc0426", size = 5793732, upload-time = "2026-06-15T11:36:35.745Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/28/8c/aff94d276e91207a87cedff7cfefdd4aca20444137cca77bf53fffebe77a/prek-0.4.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:590427a42a3c1e5064487a0dc91167ae0c8a52168e77f574758ef9b138fcfd61", size = 5521719, upload-time = "2026-06-15T11:36:39.383Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/4f/73/cfb0c5c909442050a8357e26233f7e511ba8e0d2f4b0bdc460065d62beb6/prek-0.4.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fd98b986767dafdb6b4305b563ee5a3a8f13bd3c78b98d708626815ea9f147f", size = 5922623, upload-time = "2026-06-15T11:36:18.063Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/0a/ad/ff9d26551ba80d190bd08c6341176a5d56d4e6de9c2ebf077793d4adbb78/prek-0.4.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fccd11613ae92619d1ecda0ab3359ceebeb38898909ec84a8d383733d12158cc", size = 6722071, upload-time = "2026-06-15T11:36:43.086Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d3/43/11d1dfd66c919953fe89ae2fdedd4f413ee923883043816d35982177bb75/prek-0.4.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14109d37b33e5529db41a3539d4f8f72d295f6eeddede3964994d898b8cec05c", size = 6176454, upload-time = "2026-06-15T11:36:33.803Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d7/d4/9749f25c2e0ee5225f812457b888acef301e0ccce64bebcda2ac1d04abee/prek-0.4.5-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:40d262418105b2ede9836593a1927fc927cc8093c432e998640964102196996e", size = 5791133, upload-time = "2026-06-15T11:36:23.891Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/72/5e0344bab1eacf813a5b1b082cb4c6253930096166dad51c1cccee0a4f83/prek-0.4.5-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:a586d14c3b852fdee1c3dcd0b9cb0915db9f9d054334b854fd9470bf68edf129", size = 5658098, upload-time = "2026-06-15T11:36:44.862Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/be/a5/1f406e0362dd0f18ba09a562d50d7c04a70ac05d350b1ab6fba36ca3e9f0/prek-0.4.5-py3-none-musllinux_1_1_armv7l.whl", hash = "sha256:a8ed0d28f3e7790e4402a9324c386509066df6e67cc587f7406f9a245b97b7e8", size = 5498634, upload-time = "2026-06-15T11:36:31.828Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c7/df/b0cbf0fa527330188390b7b6c8d279cd5e509923262d0a6c5cc44bbdf103/prek-0.4.5-py3-none-musllinux_1_1_i686.whl", hash = "sha256:86f76bd3d2ecf6fd9034d75c62ff4c786eb11d0dd0a1f79bbb4343b023e12769", size = 5784840, upload-time = "2026-06-15T11:36:37.481Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9d/d7/977ee3c622c906677dd94187a00392ce2dd76035486b3a3b1b5a5267dd34/prek-0.4.5-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:e491a1a4641d91d8b03dcce5588397e76d2a5b432c9b0a6c70475972b4512ab4", size = 6300384, upload-time = "2026-06-15T11:36:27.602Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1899,14 +1899,13 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "pytest-codeblocks"
|
||||
version = "0.17.0"
|
||||
version = "0.18.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "pytest", marker = "sys_platform == 'darwin' or sys_platform == 'linux'" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/a5/99/1ee3017a525dcb36566f0523938fbc20fb33ef8bf957205fafe6659f3a60/pytest_codeblocks-0.17.0.tar.gz", hash = "sha256:446e1babd182f54b4f113d567737a22f5405cade144c08a0085b2985247943d5", size = 11176, upload-time = "2023-09-17T19:17:31.05Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/fa/2c/503f797c7ac1e35d81944f8fbcf3ea7a1965e435676570a833035d8d0937/pytest_codeblocks-0.17.0-py3-none-any.whl", hash = "sha256:b2aed8e66c3ce65435630783b391e7c7ae46f80b8220d3fa1bb7c689b36e78ad", size = 7716, upload-time = "2023-09-17T19:17:29.506Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3a/cb/5f40df7db75c0ac2555009287bb97b322f8d79f9f2bfdfb0bdb560834c28/pytest_codeblocks-0.18.0-py3-none-any.whl", hash = "sha256:3fe944dc505107421204c83e9232e0155eea1279b9425a10bee327079b272efb", size = 8009, upload-time = "2026-06-15T20:48:02.608Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2666,11 +2665,11 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "tzlocal"
|
||||
version = "5.3.1"
|
||||
version = "5.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/8b/2e/c14812d3d4d9cd1773c6be938f89e5735a1f11a9f184ac3639b93cef35d5/tzlocal-5.3.1.tar.gz", hash = "sha256:cceffc7edecefea1f595541dbd6e990cb1ea3d19bf01b2809f362a03dd7921fd", size = 30761, upload-time = "2025-03-05T21:17:41.549Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/d8/52/ee2e6d7031687c5bad28363148cb72f2bbf38201d2e220671bd9fb830bc2/tzlocal-5.4.tar.gz", hash = "sha256:41e1293f80d4b5ff38dff222601a8fbd06b4fdcaf25e224704047ad26a39af54", size = 30922, upload-time = "2026-06-15T12:06:56.594Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/14/e2a54fabd4f08cd7af1c07030603c3356b74da07f7cc056e600436edfa17/tzlocal-5.3.1-py3-none-any.whl", hash = "sha256:eb1a66c3ef5847adf7a834f1be0800581b683b5608e74f86ecbcef8ab91bb85d", size = 18026, upload-time = "2025-03-05T21:17:39.857Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1d/70/5771c9ecbdb7cc0c3f3bbded7e0fa7911ee8e872ce5b5dc48ce7dce21a11/tzlocal-5.4-py3-none-any.whl", hash = "sha256:024d11221ff83453eae1f608f09b145b9779e1345d08c15404ce8ff7917cf629", size = 28261, upload-time = "2026-06-15T12:06:54.914Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2693,34 +2692,40 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "ujson"
|
||||
version = "5.12.1"
|
||||
version = "5.13.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/bc/78/937198ea8708182dd1edbf0237bf255a96feab3f511691ad08b84da98e5d/ujson-5.12.1.tar.gz", hash = "sha256:5b7e96406c301a1366534479a7352ec40ec68bb327c0c119091635acd5925e35", size = 7164538, upload-time = "2026-05-05T22:05:01.354Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/89/7a/c8bb37c8f6f3623d60c33d15d18cd6d6655d0f9c3eb31a9969f76361b199/ujson-5.13.0.tar.gz", hash = "sha256:d62e3d7625384c08082abad81a077af587fdef2761bb14c3822f4234b8d07d75", size = 7166784, upload-time = "2026-06-14T22:36:50.209Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/b9/f0/985b351771ebf095e2c1aaad18f4d251831226a767a32593310e4f181f19/ujson-5.12.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c4bdc052a5d097f0a2e56d93aed97355f9f7a62ef9baa4f8517e43245434af9c", size = 57959, upload-time = "2026-05-05T22:03:48.348Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/61/73/03c7473372e1a538206fc655e474fa15f8bf9c46bb7c73c5fec9a544e429/ujson-5.12.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5dc91fa06ea35920b704fd9d70871897680145998071cfbf5ee3e19f2c9fc242", size = 55564, upload-time = "2026-05-05T22:03:49.869Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/04/e6/104ebc35fa8dbaca66bf027c53c0c9c572271c2984576f4fd7d349d1a2e4/ujson-5.12.1-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5db0849c0e3da54822a5834f2dc51d7c51072d7f7d665014ee34600dc10889b", size = 59448, upload-time = "2026-05-05T22:03:51.224Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/11/d2/55274e80fe1806cdb5cb97483be16cd6163337ab11c3bd7e28ff8a8aad26/ujson-5.12.1-cp313-cp313-manylinux_2_24_i686.manylinux_2_28_i686.whl", hash = "sha256:949cb4863a5d4847edeb47c5364b334e8cadf23a7cbdaa547d86098a4b093106", size = 61611, upload-time = "2026-05-05T22:03:52.731Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6c/15/ec46b1757c8f7770d8c101b8a463bec67c19e89c46c608d01e4b193cc64a/ujson-5.12.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8aa731138d6dfca4ab84501b72384e6c544bfb48cb87a0dd4d304df3246cac25", size = 59120, upload-time = "2026-05-05T22:03:54.064Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b5/27/ec73bc8908c33eb1f5be29d696084e531cbcfbd5c7b89ce54c025f66c682/ujson-5.12.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:727e983ef27892d86ee2d28fd517eeb02b2c1165aafcbe929dce988aeee81bfe", size = 1038913, upload-time = "2026-05-05T22:03:55.792Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6d/30/907e47569bed5f5eb258fef5e587c6759a7a062048796e40024497137e28/ujson-5.12.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d57d731ecf492d3d011e65369f8330654f0875b19f646be5270d478e843d3b81", size = 1198409, upload-time = "2026-05-05T22:03:57.947Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/46/aa/f135f4b741baf14d5350be5511076408e7540353d3d850a430cb89d585a6/ujson-5.12.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1a09636220f26c66f80c6c6283023cb53120e843825f890be92696cd1aa43f39", size = 1091456, upload-time = "2026-05-05T22:04:00.355Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f8/ca/d88d86f90f8f237985f3e347b9a4f9fa24e8d30d19ec7d477ed18aa58393/ujson-5.12.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:6f19e9a407a24230df0cc1ec1c0f5999872ba526b14a780f80ad6479f5eed9bc", size = 58099, upload-time = "2026-05-05T22:04:06.688Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ae/2d/a0a88407cee3550f7ed1e49b41157ee2d410f51905ed51fb134844255280/ujson-5.12.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8b657e870c77aaacdeea86cfad3e6d2ef9b52517e45988c9c367f7ee764fe4dd", size = 55631, upload-time = "2026-05-05T22:04:07.925Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/6d/12a3b8e72132db244ae048075e71a0079b3c5f61ff45b7ca81d5193ab3e7/ujson-5.12.1-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:984b5a99d1e0a037c2046c3c4b34cec832565d62d5017be0a035bf3cbfab72dc", size = 59469, upload-time = "2026-05-05T22:04:09.208Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a2/72/310f8c21737554f2d2b4f1883e1a71e8a6ab0d8f92f0feb8aaa85e0f4b66/ujson-5.12.1-cp314-cp314-manylinux_2_24_i686.manylinux_2_28_i686.whl", hash = "sha256:f48ef8a16f1d85bd7982beac7adfd3fb704058631db84c1c61c8a1b7072b1508", size = 61611, upload-time = "2026-05-05T22:04:10.836Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/50/ab4b2f7bab6c7a67298c8f2aca80e2082eaf6f332cf2d099762647b5301e/ujson-5.12.1-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f39ba3b65cc637b59731532f7e7c807786bff1d0332ab2d5b96a04d2584d78f", size = 59122, upload-time = "2026-05-05T22:04:12.137Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/21/48/5d81cbe76fc2aa9e071aa489a3041cf0712f5e0663d60d501641f92b7bb4/ujson-5.12.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:07f307780f85b49cba93f291718421b6f5f3b627a323b431fad937a18f6587cb", size = 1038938, upload-time = "2026-05-05T22:04:13.548Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fb/a7/abe1acb0e5d8b8d724b35533a44c89684c88100a5fd9f2fee7f7155528d5/ujson-5.12.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:1c335caea51c31494e514b82d50763b9792d3960d2c7d9fdb6b6fb8ed50ebdd0", size = 1198416, upload-time = "2026-05-05T22:04:15.609Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/6e/087067d6ee22bd01bfba9fb1f32ce98c24ae2bcbab53bd2fbf8f7a80fe9e/ujson-5.12.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:19ea07e29a45d199f926aadf93a9974128438c01b83141fba32477c0ee604b33", size = 1091425, upload-time = "2026-05-05T22:04:17.909Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3f/3d/7547835cd0b7fa22eb1122702f81b2403c38a0027a2cc0d75acc449a4a66/ujson-5.12.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:49dd9c378e1c8e676785ff2b62cb490074229f15ab54abf45b623713cb2c36b5", size = 58565, upload-time = "2026-05-05T22:04:23.75Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ed/6a/1784e0b24aab50623eb47b2f7a8dc22c9d809d798854d2568a9cb7c3560f/ujson-5.12.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6d8827904358d7da59ccf2e1fd8de59e78248036d17fecc0462e62c6721f1102", size = 56157, upload-time = "2026-05-05T22:04:25.028Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/91/2d/2c1b24df24eee309047d81460c3a1acf0d047207327edc6f3cab8a614985/ujson-5.12.1-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc26caebea90425662ef0b979f945f6ac832651881107d6ec9a3c4d4a4ba929c", size = 60288, upload-time = "2026-05-05T22:04:26.273Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c5/14/c0c603e3dff2ef98f7deee2df7795e6055abbc5825c6ef530024b3b06a15/ujson-5.12.1-cp314-cp314t-manylinux_2_24_i686.manylinux_2_28_i686.whl", hash = "sha256:45022aae09ac3d45bda6fbfc631088d1aff9a0465542d40bd6d295ced378c430", size = 62302, upload-time = "2026-05-05T22:04:27.516Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5c/0d/889bbc044561d9adc9bf413620fbd9878f352c9fd36da829d319bca2f5ad/ujson-5.12.1-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b22aa0f644516d3d5b29464949e4b23fe784f84b4a1030ab9ac3cb42aaedabb1", size = 59784, upload-time = "2026-05-05T22:04:28.776Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/18/35/3b1d8ff8cd6dc048f5c495af6ee6ded43055562610a7e9b78b438dc6421e/ujson-5.12.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7dc5cf44ea42365cd1b66e6ed3fc6ca040c86587b024a6659b98e99d31cff2cd", size = 1039759, upload-time = "2026-05-05T22:04:30.291Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/d8/3c66cdf839420a6da2d6140a54a882c15efd135bcced103bd4473d577636/ujson-5.12.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:8df5d984ff4ac1ef292d70f30da03417038a7e1e0bc272d28ca9d34f02f41682", size = 1199121, upload-time = "2026-05-05T22:04:31.961Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/54/51/c3d1b94a4ad27dc7532e9f7d00b869463157cede2295ba6d57566afeb8cd/ujson-5.12.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:485f0182a0c0b54c304061cdc826d8343ce595c4055f7a24e72772a8520e5f7b", size = 1092085, upload-time = "2026-05-05T22:04:33.697Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a7/f1/fe8a467d8ff5821e076b96f398d3acfe3cd568d900e6ccb41b215592b152/ujson-5.13.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:46998fc8d11aec34a20e2010905e7059732a3d192d9a3c3fe4f9ffd146c87ec8", size = 56746, upload-time = "2026-06-14T22:35:43.398Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6a/c0/c7ab82d6471dfa7e4fd68ae6ff2c6a50d077c05d6ecdea0cec8af635b2c4/ujson-5.13.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ee03ce288ba25b05cf0de87203165642277a25caa4f00a437e13152e5214e310", size = 54388, upload-time = "2026-06-14T22:35:44.586Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/10/e6/4e9e998d991ff88bbc93b21daa63bba2baa61c6f952dbcec937cf7304ebe/ujson-5.13.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:cdf33b588a81b05d0b585c66f83050c49cb670623424d10e4d1ad37ba2f7eed9", size = 60051, upload-time = "2026-06-14T22:35:45.567Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9c/11/876dff43f05417a01c6119f0fa10e01f1226631c5927ef08f56876b2bb67/ujson-5.13.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4cabd73c114ce93c21d7db2e2d8e16217fd8a5b2b3ec754629eebef5c262d47f", size = 53438, upload-time = "2026-06-14T22:35:46.623Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/09/02/f9dbf6c3e46d700eb1d9ed637567221a06eeb1ec289633be992ef54d7a34/ujson-5.13.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ffc61fc756a64f4d169a78cc638d769e3c324f45fc51997626abf4e5e5dd6460", size = 55060, upload-time = "2026-06-14T22:35:47.647Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/3d/7e49a70265a1e5ed1b5e8edd5f54d57ae41e2134faeae9b16f6f5a0eae20/ujson-5.13.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c00323c13a35822c9a67a26c0b2a0787510bf1ef490922b58009b362d1a3e21", size = 58189, upload-time = "2026-06-14T22:35:48.617Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/66/34/b64278f67e19052f09810576c7e50b3da8d4f5218b226046324d4d5c24b4/ujson-5.13.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:496e662a6b46d5f936d77fb68259cece19213bb2301ddd520dbd75ac7c90c5f4", size = 57941, upload-time = "2026-06-14T22:35:49.674Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a9/8c/51513357a5c75bf3e5bae46accfdb3e6e6f5caeb72ca8b253ec45ba853fb/ujson-5.13.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7fd41b86444df14f8b4b7afaaa9f27bacfbf8c18380872317aeab6cd125dcede", size = 1037688, upload-time = "2026-06-14T22:35:50.699Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/54/5a/dc6afe071d6b977390d2dc41e15800a2716f317988dd03187cffe7b4d624/ujson-5.13.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:bf3c2c4ea55d4187903fcdc689a9bf5b0fc72d8c0eaff39db18c1f337c8832c1", size = 1197141, upload-time = "2026-06-14T22:35:52.052Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/50/23/b473d101412c68527cb502a8728f96ab307aa7bfa75d6ea2037e2c7f74e8/ujson-5.13.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b6eca7751d61045a9b1e7f9a8c97ac24b164f085b60bef1c4668654bb2338011", size = 1090235, upload-time = "2026-06-14T22:35:53.589Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/1c/9a/b5139d696f5328f3cab70b9ec046f15e3f49497a4de6280974640602f539/ujson-5.13.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:cc9dfd41fed397ab03bb9d9fe1cbd83301211c772a17536033ce7d68877ac82b", size = 56897, upload-time = "2026-06-14T22:35:57.974Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/53/55/477183aeddfdf0f88ae039ffee0ed866cfb993da0c0c9aa915807554aef8/ujson-5.13.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ca7ef2fa6c408a7c0f558e4d33d93b32ddc35ed6d3cfc505747931a64b7465d5", size = 54451, upload-time = "2026-06-14T22:35:58.932Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ea/63/55e5f23e156b4c8bca095d828b4cd3180c0b42aa3501ef88836d79606fea/ujson-5.13.0-cp314-cp314-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:a554b2e5bee85030369514cef8b0b913cebe1a4c2c0c13541966d50bcba22b1a", size = 60053, upload-time = "2026-06-14T22:35:59.969Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/26/b6/08c6cf5548bd6f4bb557c9fa7e8edf87324bb04c17249d1966028d61dde0/ujson-5.13.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ea939ff629ab03ae970d03eca6d1febd8ed55ba38ca44aec64ce997537cd3fa0", size = 53481, upload-time = "2026-06-14T22:36:01.007Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6e/b3/0ac9a03551467784067f505df1bb875c639ba32f1da79ce467ab15911ada/ujson-5.13.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b98bf2faa5e37ecfe752226ea08290031e375a0c43d425a0b955fb3e702a2a71", size = 55058, upload-time = "2026-06-14T22:36:02.297Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ba/be/ec91029aec067174473d022fa0f6c3c1431a173f888d7599739f05c668eb/ujson-5.13.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9a4b92344b16e414aeb609e57f62c466500e53c94f1698f5b149dc0b7223ec3e", size = 58225, upload-time = "2026-06-14T22:36:03.321Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/29/33/a948f329252ece3f9c93d177243de6e677927ebc6ac44256742dbbef3c39/ujson-5.13.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7df805aad707507a1fa165fb716218ca3a89f142125dc4b23c9fcc08fa402d97", size = 57930, upload-time = "2026-06-14T22:36:04.385Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f1/0c/c33655218b8e0a8adbf066de0b999cae5c324061f3eaa4dda17423145d9e/ujson-5.13.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7576bdbef327c3528f011002a2d74486f6fe4e33289bdb7a042b7f1a6e9d8285", size = 1037728, upload-time = "2026-06-14T22:36:05.467Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2c/bd/d286947525ea7ce3f2d8dc55c15b9ffbe425bc455c96af7b8f8a402599a9/ujson-5.13.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:6eee5d7cce3f32a468905f9ff61807a60287a90258d849460f6fa826e810870d", size = 1197146, upload-time = "2026-06-14T22:36:06.839Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/3c/3c/9eb916377050b0785f048a34588c1c390ddd41ae00b78db68ee1ad022356/ujson-5.13.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:144e9d8a454cfa727e0f755e1863738ed68068583bda5463052cb446835bd56c", size = 1090223, upload-time = "2026-06-14T22:36:08.329Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/12/e9/1c543837c6a3c6672361882a0fa269bd02daf9cc4c0ca88a9dccd9df98d9/ujson-5.13.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:69b4e36bb7d5f413ba8c00c8006b2ec627cc5ace97301462f6aadb66ec9d2979", size = 57402, upload-time = "2026-06-14T22:36:13.238Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/e4/39862f0f7174ff07cfd1e2d0c9065ded34aeebdb7db8daf2f0e5bf89b46f/ujson-5.13.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8b644d50f66de5490c1823c7176618cead5e8e8a88cba9f40a6308ca52e79267", size = 54973, upload-time = "2026-06-14T22:36:14.432Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/02/66/f53d3b32c3f177f846ca6b624e832f29000d8a213a2d8768e254bd470ced/ujson-5.13.0-cp314-cp314t-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:15107aaa4f559d55201165ec32abb35c283a861be1fa67229578cb7d93fcd93a", size = 60683, upload-time = "2026-06-14T22:36:15.806Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e6/d4/dddc4646d2633c85c938c2ded7d5a9711cdad5be1e13b31b7dad76f61c83/ujson-5.13.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6e343c5f0c058523f1edbf6ae4eceb4e0d934205a53bbdd8d9a945c83324662a", size = 54167, upload-time = "2026-06-14T22:36:16.952Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/5f/c0/d8608c3f4d3f05e6441364b63fde1d279700135c1a6577a773662c07fbcc/ujson-5.13.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:02200035bc80e830f076ffc1b329a94c295aee6d9de8c9043647cb9a7bd4f76f", size = 55568, upload-time = "2026-06-14T22:36:17.975Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/22/8e/dd12b735aaba0806c3d70c18184d50e1f9712e0757c7c0a4f376450cfe28/ujson-5.13.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d7f19b81b73ff28f5c5022ee794f94122bfcda07a76423078e349465d71223a1", size = 59086, upload-time = "2026-06-14T22:36:19.071Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/48/43/ad41e8752d5ec3a590a5e7b426a54e36b7aab911d9b5a4f7384dc62507ab/ujson-5.13.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:82e1393e6dbe3c95fdfc95c6c528890e191351a1f024ef51126cf1f22543af52", size = 58667, upload-time = "2026-06-14T22:36:20.171Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e1/8e/b44a6afb77b94118655c029081b7932d64bb4c5b1c8ba2b7f5808b5d0bc2/ujson-5.13.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:38afcf994b28ed85ea2420e2a8d79a37d0a77348b3daf53850c16edda66f942d", size = 1038553, upload-time = "2026-06-14T22:36:21.245Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7e/93/fab1d786174c8780eb3e386c73f1925a435e97fbf77c957fea4fca83994d/ujson-5.13.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:1bdf2518971586f2b413156c49d9dd8b56cc990a8647081e1bd00af60564d469", size = 1197938, upload-time = "2026-06-14T22:36:22.585Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f3/bc/2f073bb708f9d128f5d1cb39063a5f6421b1ce94c61be8661c55a189f407/ujson-5.13.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:751ad01042472f1c7c02f5c597c7aee79834e82a6cc384ca302173bbc8e8deb8", size = 1090938, upload-time = "2026-06-14T22:36:23.947Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
Reference in new issue
Block a user