Repository navigation
Bump blastdns to 2.0.4 - #3631
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## dev #3631 +/- ##
=====================================
- Coverage 91% 91% -0%
=====================================
Files 465 465
Lines 51927 51927
=====================================
- Hits 47048 47040 -8
- Misses 4879 4887 +8 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
📊 Performance Benchmark Report
📈 Detailed Results (All Benchmarks)
🎯 Performance Summary+ 1 improvement 🚀
30 unchanged ✅🔍 Significant Changes (>10%)
🐍 Python Version 3.11.18 |
en0f
left a comment
There was a problem hiding this comment.
Reviewed at 32f3915.
🟢 Clean, reproducible lock bump
Re-locking the base with uv lock --upgrade-package blastdns==2.0.4 (uv 0.13.7) reproduces this uv.lock byte for byte, so only the blastdns entry and its specifier move. All 71 locked artifacts match PyPI's hashes and carry PyPI provenance from blastdns publish.yml at 58976d2d (the blastdns#179 merge), the wheel tags are exactly the 70 the base locked, and the sdist's src/, blastdns/ and Cargo files equal that commit.
Reproduce this review
# save the script below as verify-3631.sh and the patch as control-3631.patch, then:
bash verify-3631.sh blacklanternsecurity/bbot 32f39151518c37598c4379e56bbb3a2df4c29715
# expected: SUMMARY 12 PASS, 0 FAIL, 0 SKIP, 0 BROKEN
# control: bash verify-3631.sh blacklanternsecurity/bbot 32f39151518c37598c4379e56bbb3a2df4c29715 control-3631.patch -> V01 V02 V03 V04 V05 V12 FAIL#!/usr/bin/env bash
# verify-3631.sh: reproducible checks for blacklanternsecurity/bbot#3631
# Usage: bash verify-3631.sh <owner/repo> <sha> [<control-patch>]
# Changes nothing outside a mktemp directory and nothing on GitHub (PR-VF-02).
#
# CONTROL: patch control-3631.patch
# CONTROL-EXPECT: V01=FAIL V02=FAIL V03=FAIL V04=FAIL V05=FAIL V12=FAIL
# AUTHOR-CODE: none
set -uo pipefail
REPO="${1:?owner/repo}"; SHA="${2:?sha}"; PATCH="${3:-}"
BASE_SHA=3085baacf1f812fcf446d73f673aa2454b199ce4 # PR base (dev)
BLASTDNS_REL=58976d2d2f42300bd040f9f615ed3277bb92969c # blastdns#179 merge into stable (2.0.4)
PASS=0; FAIL=0; SKIP=0; BROKEN=0
pass() { echo "PASS $1 $2"; PASS=$((PASS+1)); }
fail() { echo "FAIL $1 $2 -- $3"; FAIL=$((FAIL+1)); }
skip() { echo "SKIP $1 $2 -- $3"; SKIP=$((SKIP+1)); }
broken() { echo "BROKEN $1 $2 -- $3"; BROKEN=$((BROKEN+1)); }
summary() { echo "SUMMARY $PASS PASS, $FAIL FAIL, $SKIP SKIP, $BROKEN BROKEN"; [ "$FAIL" -eq 0 ] && [ "$BROKEN" -eq 0 ]; }
echo "script_sha256 $(shasum -a 256 "$0" | cut -d' ' -f1)"
echo "git $(git --version | cut -d' ' -f3)"
echo "python3 $(python3 -c 'import sys; print(sys.version.split()[0])')"
case "$PATCH" in ""|/*) ;; *) PATCH="$(cd "$(dirname "$PATCH")" && pwd)/$(basename "$PATCH")";; esac
WORK="$(mktemp -d)"; trap 'rm -rf "$WORK"' EXIT
export GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_NOSYSTEM=1 GIT_TERMINAL_PROMPT=0
GIT=(git -c credential.helper= -c 'credential.helper=!gh auth git-credential')
"${GIT[@]}" clone --quiet "https://github.com/$REPO.git" "$WORK/src" --no-checkout || { broken V00 "clone" "clone failed"; summary; exit 1; }
cd "$WORK/src"
"${GIT[@]}" cat-file -e "$SHA^{commit}" 2>/dev/null || "${GIT[@]}" fetch --quiet origin "+refs/pull/*/head:refs/remotes/pr/*" || true
"${GIT[@]}" checkout --quiet --detach "$SHA" || { broken V00 "checkout $SHA" "checkout failed"; summary; exit 1; }
[ "$(git rev-parse HEAD)" = "$SHA" ] || { broken V00 "HEAD is $SHA" "HEAD mismatch"; summary; exit 1; }
if [ -n "$PATCH" ]; then git apply "$PATCH" || { broken V00 "apply control patch" "git apply failed"; summary; exit 1; }; fi
# ---- pinned uv, checksum-verified, with wrong-hash control (PR-VF-03) --------
UV_URL=https://github.com/astral-sh/uv/releases/download/0.13.7/uv-aarch64-apple-darwin.tar.gz
UV_SHA256=81b811b57e4624df7ed3aac148e0359100d9962f3d9dce00aec533dc2e086731
# UV_NO_CONFIG isolates uv; XDG_CONFIG_HOME is not exported because it hides gh auth
export UV_CACHE_DIR="$WORK/uv/cache" UV_PYTHON_INSTALL_DIR="$WORK/uv/python" UV_NO_CONFIG=1
UV=""
if curl -fsSLo "$WORK/uv.tgz" "$UV_URL"; then
if echo "$UV_SHA256 $WORK/uv.tgz" | shasum -a 256 -c - >/dev/null 2>&1; then
pass V10 "uv 0.13.7 tarball matches its published sha256"
tar xzf "$WORK/uv.tgz" -C "$WORK" && UV="$WORK/uv-aarch64-apple-darwin/uv"
echo "uv $("$UV" --version)"
else broken V10 "uv 0.13.7 tarball matches its published sha256" "mismatch"; fi
if echo "0000000000000000000000000000000000000000000000000000000000000000 $WORK/uv.tgz" | shasum -a 256 -c - >/dev/null 2>&1; then
broken V11 "wrong-hash control rejects uv" "accepted a wrong hash"
else pass V11 "wrong-hash control rejects uv"; fi
else skip V10 "uv 0.13.7 tarball matches its published sha256" "download failed"; fi
# ---- the blastdns block of uv.lock, as data ---------------------------------
lockblock() { awk '/^name = "blastdns"$/{f=1} f&&/^\[\[package\]\]$/{exit} f' "$1"; }
lockblock uv.lock > "$WORK/head_block"
git show "${BASE_SHA}:uv.lock" > "$WORK/base_lock"; lockblock "$WORK/base_lock" > "$WORK/base_block"
LOCKVER=$(sed -n 's/^version = "\(.*\)"$/\1/p' "$WORK/head_block" | head -1)
echo "lock_blastdns_version $LOCKVER"
# V01: pyproject floor, the lock's requires-dist specifier and the locked version agree on 2.0.4
PYSPEC=$(sed -n 's/^ *"blastdns\(.*\)",$/\1/p' pyproject.toml)
LKSPEC=$(sed -n 's/^ *{ name = "blastdns", specifier = "\(.*\)" },$/\1/p' uv.lock)
if [ "$PYSPEC" = ">=2.0.4,<2.1" ] && [ "$LKSPEC" = "$PYSPEC" ] && [ "$LOCKVER" = "2.0.4" ]; then
pass V01 "pyproject requires blastdns>=2.0.4,<2.1, uv.lock records the same specifier and locks 2.0.4"
else fail V01 "pyproject requires blastdns>=2.0.4,<2.1, uv.lock records the same specifier and locks 2.0.4" "pyproject='$PYSPEC' lock-spec='$LKSPEC' locked='$LOCKVER'"; fi
# V02: re-locking the base with this tree's pyproject and pinned uv reproduces uv.lock byte for byte
if [ -z "$UV" ]; then skip V02 "uv.lock is exactly what uv 0.13.7 produces from the base for blastdns $LOCKVER" "uv unavailable"
else
git worktree add --quiet --detach "$WORK/relock" "$BASE_SHA"
cp pyproject.toml "$WORK/relock/pyproject.toml"
if (cd "$WORK/relock" && "$UV" lock --upgrade-package "blastdns==$LOCKVER" >"$WORK/relock.log" 2>&1); then
if cmp -s "$WORK/relock/uv.lock" uv.lock; then pass V02 "uv.lock is exactly what uv 0.13.7 produces from the base for blastdns $LOCKVER"
else fail V02 "uv.lock is exactly what uv 0.13.7 produces from the base for blastdns $LOCKVER" "$(diff "$WORK/relock/uv.lock" uv.lock | grep -c '^[<>]') differing lines"; fi
else skip V02 "uv.lock is exactly what uv 0.13.7 produces from the base for blastdns $LOCKVER" "uv lock failed: $(tail -1 "$WORK/relock.log")"; fi
fi
# V03: every blastdns artifact hash in uv.lock equals PyPI's published sha256 for that file
if curl -fsSL "https://pypi.org/pypi/blastdns/$LOCKVER/json" -o "$WORK/pypi.json"; then
python3 - "$WORK/head_block" "$WORK/pypi.json" > "$WORK/v03" <<'PY'
import json, re, sys
block = open(sys.argv[1]).read()
pypi = {u["filename"]: u["digests"]["sha256"] for u in json.load(open(sys.argv[2]))["urls"]}
pairs = re.findall(r'url = "[^"]*/([^/"]+)", hash = "sha256:([0-9a-f]{64})"', block)
bad = [f for f, h in pairs if pypi.get(f) != h]
print(len(pairs), len(bad), " ".join(bad[:3]))
PY
read -r NPAIRS NBAD BADF < "$WORK/v03"
if [ "${NPAIRS:-0}" -gt 0 ] && [ "${NBAD:-1}" -eq 0 ]; then pass V03 "all $NPAIRS blastdns $LOCKVER hashes in uv.lock match PyPI"
else fail V03 "every blastdns hash in uv.lock matches PyPI" "$NBAD of $NPAIRS differ: $BADF"; fi
else skip V03 "every blastdns hash in uv.lock matches PyPI" "PyPI JSON unavailable"; fi
# V04: the locked wheel set covers every platform/interpreter tag the base lock covered
tags() { grep -oE 'blastdns-[0-9.]+-[^"/]+\.whl' "$1" | sed -E 's/^blastdns-[0-9.]+-//' | sort -u; }
tags "$WORK/base_block" > "$WORK/base_tags"; tags "$WORK/head_block" > "$WORK/head_tags"
MISSING=$(comm -23 "$WORK/base_tags" "$WORK/head_tags" | tr '\n' ' '); EXTRA=$(comm -13 "$WORK/base_tags" "$WORK/head_tags" | tr '\n' ' ')
if [ -s "$WORK/base_tags" ] && [ -z "$MISSING" ] && [ -z "$EXTRA" ]; then pass V04 "uv.lock locks exactly the same $(wc -l < "$WORK/base_tags" | tr -d ' ') blastdns wheel tags as the base"
else fail V04 "uv.lock locks exactly the blastdns wheel tags the base locked" "missing: $MISSING extra: $EXTRA"; fi
# V05: the sdist uv.lock pins downloads at its locked hash, and its Rust and Python sources equal blastdns at the 2.0.4 release merge
SD_URL=$(sed -n 's/^sdist = { url = "\([^"]*\)", hash = "sha256:\([0-9a-f]*\)".*/\1/p' "$WORK/head_block")
SD_SHA=$(sed -n 's/^sdist = { url = "\([^"]*\)", hash = "sha256:\([0-9a-f]*\)".*/\2/p' "$WORK/head_block")
if curl -fsSLo "$WORK/sdist.tgz" "$SD_URL"; then
if echo "$SD_SHA $WORK/sdist.tgz" | shasum -a 256 -c - >/dev/null 2>&1; then
mkdir -p "$WORK/sdist" && tar xzf "$WORK/sdist.tgz" -C "$WORK/sdist"
SDIR=$(ls -d "$WORK"/sdist/blastdns-*)
if ! { "${GIT[@]}" clone --quiet "https://github.com/blacklanternsecurity/blastdns.git" "$WORK/bd" && (cd "$WORK/bd" && git checkout --quiet --detach "$BLASTDNS_REL") && [ "$(git -C "$WORK/bd" rev-parse HEAD)" = "$BLASTDNS_REL" ]; }; then
broken V05 "locked sdist sources equal blastdns at ${BLASTDNS_REL:0:8}" "could not check out blastdns at $BLASTDNS_REL"
else
DIFFS=$(diff -rq "$SDIR/src" "$WORK/bd/src"; diff -rq "$SDIR/blastdns" "$WORK/bd/blastdns"; diff -q <(grep -v '^readme = "README.md"$' "$SDIR/Cargo.toml") <(grep -v '^readme = "README.md"$' "$WORK/bd/Cargo.toml") >/dev/null || echo "Cargo.toml differs"; diff -q "$SDIR/Cargo.lock" "$WORK/bd/Cargo.lock")
if [ -z "$DIFFS" ]; then pass V05 "locked sdist matches its hash, and src/, blastdns/, Cargo.toml (less the readme line maturin adds), Cargo.lock equal blastdns at ${BLASTDNS_REL:0:8}"
else fail V05 "locked sdist sources equal blastdns at ${BLASTDNS_REL:0:8}" "$(echo "$DIFFS" | head -3 | tr '\n' ' ')"; fi
fi
else fail V05 "locked sdist downloads at its locked hash" "sha256 mismatch for $SD_URL"; fi
else skip V05 "locked sdist downloads at its locked hash" "download failed"; fi
# V06: bbot's main DNS client is always built with an explicit resolver list (blastdns's empty-list path is not taken while the OS reports resolvers)
if grep -q 'self.system_resolvers = get_system_resolvers()' bbot/core/helpers/dns/dns.py \
&& grep -q 'self.resolvers = list(self.system_resolvers)' bbot/core/helpers/dns/dns.py \
&& grep -q 'self.blastdns = Client($' bbot/core/helpers/dns/dns.py \
&& awk '/self.blastdns = Client\($/{getline; print; exit}' bbot/core/helpers/dns/dns.py | grep -q 'self.resolvers,'; then
pass V06 "bbot builds its blastdns Client from get_system_resolvers() or dns.nameservers, never an empty list by design"
else fail V06 "bbot builds its blastdns Client from an explicit resolver list" "construction differs"; fi
# V07: in the 2.0.4 sdist, Python get_system_resolvers() returns bare ip.to_string(), and parse_resolver maps a bare IPv6 to port 53
if [ -n "${SDIR:-}" ] && grep -q 'resolver_ips.iter().map(|ip| ip.to_string()).collect()' "$SDIR/src/python.rs" \
&& awk '/fn parse_resolver/,/^}/' "$SDIR/src/utils.rs" | grep -q 'return Ok(SocketAddr::new(ip, 53));'; then
pass V07 "blastdns $LOCKVER: Python get_system_resolvers() returns bare IPs and parse_resolver gives a bare IPv6 port 53"
elif [ -z "${SDIR:-}" ]; then skip V07 "blastdns get_system_resolvers()/parse_resolver handle bare IPv6" "sdist unavailable"
else fail V07 "blastdns get_system_resolvers()/parse_resolver handle bare IPv6" "code differs from the reviewed form"; fi
# V12: every locked blastdns artifact has PyPI (PEP 740) provenance whose subject digest is the locked hash,
# published by blastdns publish.yml, with a signing certificate naming the release commit. Trusts PyPI's served bundle.
echo "openssl $(openssl version)"
awk '/^name = "blastdns"$/{f=1} f&&/^\[\[package\]\]$/{exit} f' uv.lock \
| grep -oE 'url = "[^"]*/(blastdns-[^"/]+)", hash = "sha256:[0-9a-f]{64}"' \
| sed -E 's|url = "[^"]*/([^"/]+)", hash = "sha256:([0-9a-f]{64})"|\1 \2|' > "$WORK/pairs"
PN=0; PBAD=0; PFIRST=""
while read -r F H; do
PN=$((PN+1))
if ! curl -fsS -H 'Accept: application/vnd.pypi.integrity.v1+json' "https://pypi.org/integrity/blastdns/$LOCKVER/$F/provenance" -o "$WORK/p.json"; then PBAD=$((PBAD+1)); PFIRST="${PFIRST:-noprov $F}"; continue; fi
if ! python3 -I - "$WORK/p.json" "$H" "$WORK/c.der" <<'PY'
import json,base64,sys
d=json.load(open(sys.argv[1])); b=d["attestation_bundles"][0]; a=b["attestations"][0]
st=json.loads(base64.b64decode(a["envelope"]["statement"]))
ok = st["subject"][0]["digest"]["sha256"]==sys.argv[2] and b["publisher"]["repository"]=="blacklanternsecurity/blastdns" and b["publisher"]["workflow"]=="publish.yml"
open(sys.argv[3],"wb").write(base64.b64decode(a["verification_material"]["certificate"]))
sys.exit(0 if ok else 1)
PY
then PBAD=$((PBAD+1)); PFIRST="${PFIRST:-digest/publisher mismatch $F}"; continue; fi
openssl x509 -inform der -in "$WORK/c.der" -noout -text 2>/dev/null | grep -aq "$BLASTDNS_REL" || { PBAD=$((PBAD+1)); PFIRST="${PFIRST:-cert not at release $F}"; }
done < "$WORK/pairs"
if [ "$PN" -gt 0 ] && [ "$PBAD" -eq 0 ]; then pass V12 "all $PN locked blastdns artifacts carry PyPI provenance from blastdns publish.yml at ${BLASTDNS_REL:0:8}"
else fail V12 "every locked blastdns artifact carries PyPI provenance from blastdns publish.yml at ${BLASTDNS_REL:0:8}" "$PBAD of $PN: $PFIRST"; fi
# V08/V09: CI at the head (check runs, read-only)
if gh api --method GET "repos/$REPO/commits/$SHA/check-runs?per_page=100" > "$WORK/cr.json" 2>/dev/null; then
read -r DT DOK < <(python3 -c 'import json,sys; r=[c for c in json.load(open(sys.argv[1]))["check_runs"] if c["name"].startswith("test-distros (")]; print(len(r), sum(c["conclusion"]=="success" for c in r))' "$WORK/cr.json")
if [ "$DT" -ge 6 ] && [ "$DT" = "$DOK" ]; then pass V08 "all $DT test-distros jobs (uv sync + e2e tests) succeeded at the head"
else fail V08 "all test-distros jobs succeeded at the head" "$DOK of $DT succeeded"; fi
read -r TT TOK TPEND < <(python3 -c 'import json,re,sys; r=[c for c in json.load(open(sys.argv[1]))["check_runs"] if re.fullmatch(r"test \(3\.\d+\)",c["name"])]; print(len(r), sum(c["conclusion"]=="success" for c in r), sum(c["status"]!="completed" for c in r))' "$WORK/cr.json")
if [ "$TT" -ge 5 ] && [ "$TT" = "$TOK" ]; then pass V09 "all $TT test (3.x) suite jobs succeeded at the head"
elif [ "$TPEND" -gt 0 ]; then skip V09 "all test (3.x) suite jobs succeeded at the head" "$TPEND of $TT still running"
else fail V09 "all test (3.x) suite jobs succeeded at the head" "$TOK of $TT succeeded"; fi
else skip V08 "test-distros jobs succeeded at the head" "check-runs read failed"; skip V09 "test (3.x) jobs succeeded at the head" "check-runs read failed"; fi
summarydiff --git a/pyproject.toml b/pyproject.toml
index 3a806d6a6..ca939fb27 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -44,7 +44,7 @@ dependencies = [
"tldextract>=5.3.0,<6",
"cloudcheck>=11.2.0,<12",
"blasthttp>=0.10.1,<0.11",
- "blastdns>=2.0.4,<2.1",
+ "blastdns>=2.0.3,<2.1",
"zstandard>=0.22,<1",
"packaging>=23.0",
"requests>=2.32.0,<3",
diff --git a/uv.lock b/uv.lock
index f931407ad..0eeb0e8cb 100644
--- a/uv.lock
+++ b/uv.lock
@@ -335,7 +335,7 @@ dependencies = [
{ name = "orjson" },
{ name = "pydantic" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/8a/7e/6135d0979029ba963f65f293a26aaa73ae6e31736c3732df50ddf4e990db/blastdns-2.0.4.tar.gz", hash = "sha256:5e3bb545b90a0ff010b2e1cd92c7dff02e7ac2a560abc97891325334740ccef0", size = 209578, upload-time = "2026-11-10T21:28:48.485Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/8a/7e/6135d0979029ba963f65f293a26aaa73ae6e31736c3732df50ddf4e990db/blastdns-2.0.4.tar.gz", hash = "sha256:0000000000000000000000000000000000000000000000000000000000000000", size = 209578, upload-time = "2026-11-10T21:28:48.485Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b5/91/88d88948d29fa4a1cf5707e237004582a9c082cfc772354a59944ccd3f18/blastdns-2.0.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:dadd31a86c43fc95b31710fd96ed9e6267895f2cbe516b206dd965d9aa9a88be", size = 2867979, upload-time = "2026-11-10T21:26:30.06Z" },
{ url = "https://files.pythonhosted.org/packages/8f/a2/a056a901b529a89c055af21a8c3d057230c2030a9b01ee9e96f4752668f6/blastdns-2.0.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:379ddcee5e8094d3641ab7e2d31da508cf792cb237d5bae951132a5f28912456", size = 2744116, upload-time = "2026-11-10T21:26:31.662Z" },
@@ -358,7 +358,7 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/cc/ad/c1260468e38c533fe798486c587fe1a7c2446445a55a8f34d0e157fda595/blastdns-2.0.4-cp312-cp312-manylinux_2_28_i686.whl", hash = "sha256:73eddf683ad7bf0b66f6a77012d857ab3d3ebffb829ce9661113391cea584d7e", size = 2907526, upload-time = "2026-11-10T21:26:59.698Z" },
{ url = "https://files.pythonhosted.org/packages/5f/4c/f8ae7fd2975e397961abb7e83ea677170f6ca188c84d9fc5b32d7f0fc8f8/blastdns-2.0.4-cp312-cp312-manylinux_2_28_ppc64le.whl", hash = "sha256:e85914e607b0bf0aa25f360f9aff2943aa11a8daa987d0030434d8354dc2c741", size = 3904610, upload-time = "2026-11-10T21:27:01.138Z" },
{ url = "https://files.pythonhosted.org/packages/85/ee/c3ecab2392911d1cefd73ad26037b8219b0177f00063ba60754f3ce2b11b/blastdns-2.0.4-cp312-cp312-manylinux_2_28_s390x.whl", hash = "sha256:78494e71743e8fb3bfd2eed4284daca5a7a25a552f39f33ed647945c85cd6940", size = 2854521, upload-time = "2026-11-10T21:27:02.905Z" },
- { url = "https://files.pythonhosted.org/packages/7f/ef/77e48f6cdd95bd338e20523c848ef66055b77852ecb793f230e3f1251390/blastdns-2.0.4-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:3b34b5618be365f7c6a3cd4f99b8cee6ae8faf3ef7114e93436bebf030dc65cb", size = 2706886, upload-time = "2026-11-10T21:27:04.37Z" },
+ { url = "https://files.pythonhosted.org/packages/7f/ef/77e48f6cdd95bd338e20523c848ef66055b77852ecb793f230e3f1251390/blastdns-2.0.4-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:1111111111111111111111111111111111111111111111111111111111111111", size = 2706886, upload-time = "2026-11-10T21:27:04.37Z" },
{ url = "https://files.pythonhosted.org/packages/e7/b2/e98cc7da7be8ff2e18cb36424e7c5702179b6a052a63c9dd47012f111a7c/blastdns-2.0.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7c055d3c1053d8a07e2122a2f19e0b9c6270dc73dd72b8a266a3be8c980e5846", size = 2956689, upload-time = "2026-11-10T21:27:05.972Z" },
{ url = "https://files.pythonhosted.org/packages/37/1a/52292c3ba99402f495a8bf632df61421c9186fd18167c8d2d905daaa7a75/blastdns-2.0.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ae916ca953400d3e10a59713b4d07a2b0ef79544db75738dc3fb9311e8d9786b", size = 2925210, upload-time = "2026-11-10T21:27:07.451Z" },
{ url = "https://files.pythonhosted.org/packages/ef/8c/7b9cd12e54d3bc153f508b704b5bbbe46aa9a70fa19c9e21e7dd316fc94c/blastdns-2.0.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:44b31888a613413040a440ac600f1ee9de59eada05443548573c4e29f623bd41", size = 3015777, upload-time = "2026-11-10T21:27:09.038Z" },
@@ -376,7 +376,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/e5/0b/da52bba02d097016a50deec05212a8691ccbc350def5809981daa90e1e5d/blastdns-2.0.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:31b908b31281c82db125a986ccdd7ddd9c701af719d1e7d7ca2ff19155ae08f2", size = 2956837, upload-time = "2026-11-10T21:27:29.08Z" },
{ url = "https://files.pythonhosted.org/packages/f4/09/45daee8a934de1e6af26d6ab52de831808af6e37a4e2b97065550aa84dc7/blastdns-2.0.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:62f0f18f9dd51d99fc9b09dea673994c4b4b94d72b0393932bc521223d89496e", size = 2920863, upload-time = "2026-11-10T21:27:30.611Z" },
{ url = "https://files.pythonhosted.org/packages/96/8c/753aa8188fe06676c1f2bd99e15d5d62ba49e71c3374d2091d7468c9a573/blastdns-2.0.4-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:c8d0aac05953dd418bd869e83292680d8e2ba90adf613bcdd110dd47b8d9f135", size = 3014291, upload-time = "2026-11-10T21:27:32.227Z" },
- { url = "https://files.pythonhosted.org/packages/da/1a/5c6b5a21082f0404730a5c7370bf4ba830c279cf3c4f0a478e6745714ea9/blastdns-2.0.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e877a9504fa8bef051362e877accaa40dc79f082d4f41917a5accbc568ea6aa4", size = 3019773, upload-time = "2026-11-10T21:27:33.848Z" },
{ url = "https://files.pythonhosted.org/packages/80/49/30a3f04dfe2eb6edd499a74ca264bc87cc2a50655ecd8b6b91f950f5cdfd/blastdns-2.0.4-cp313-cp313-win32.whl", hash = "sha256:199326243e93cb6eec06b0fdcb5e707e4107425cfa806a782b41ca764e7f9037", size = 2447416, upload-time = "2026-11-10T21:27:35.409Z" },
{ url = "https://files.pythonhosted.org/packages/af/78/e7ab5a08757909e518e2208d51e89f0cdde431e4be3641db41c6a0b02566/blastdns-2.0.4-cp313-cp313-win_amd64.whl", hash = "sha256:0def5f7b3d7b9b241c82168b327d214ec3f8031e80319b03c4f090b32e8991eb", size = 2955307, upload-time = "2026-11-10T21:27:36.992Z" },
{ url = "https://files.pythonhosted.org/packages/55/56/767fcf959a89d1ac29a8474e573d0723d575ded3585601481c3659c65299/blastdns-2.0.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:74065c13c56dc3f876ecc3aa9ebf14a0c30ef4e2773b6cc5a325ccd3f43a82a9", size = 2871248, upload-time = "2026-11-10T21:27:38.49Z" },
Raises the blastdns floor to 2.0.4 and re-locks.
Changes in 2.0.4: