rename package netcheck to netsleuth (name taken on pypi)

This commit is contained in:
Andrew Pimenov 2026-08-09 11:43:00 +03:00
parent 0336be4080
commit 483989c1bc
45 changed files with 163 additions and 163 deletions

View file

@ -1,11 +1,11 @@
[project]
name = "netcheck"
name = "netsleuth"
version = "0.1.0"
description = "Cross-platform CLI for deep network diagnostics: ISP/VPN/ASN identity, latency, path and bandwidth"
readme = "README.md"
license = "PolyForm-Noncommercial-1.0.0"
requires-python = ">=3.10"
authors = [{ name = "netcheck contributors" }]
authors = [{ name = "netsleuth contributors" }]
keywords = ["network", "diagnostics", "traceroute", "bgp", "vpn", "speedtest"]
dependencies = [
"typer>=0.12",
@ -24,7 +24,7 @@ tcptrace = ["scapy>=2.5"]
ndt7 = ["websockets>=12"]
[project.scripts]
netcheck = "netcheck.cli:main"
netsleuth = "netsleuth.cli:main"
[dependency-groups]
dev = [
@ -38,9 +38,9 @@ requires = ["hatchling"]
build-backend = "hatchling.build"
[tool.hatch.build.targets.wheel]
packages = ["src/netcheck"]
packages = ["src/netsleuth"]
[tool.pytest.ini_options]
testpaths = ["tests"]
asyncio_mode = "auto"
filterwarnings = ["error::DeprecationWarning:netcheck.*"]
filterwarnings = ["error::DeprecationWarning:netsleuth.*"]

View file

@ -18,7 +18,7 @@ colorama==0.4.6 ; sys_platform == 'win32'
# pytest
# typer
dnspython==2.8.0
# via netcheck
# via netsleuth
exceptiongroup==1.3.1 ; python_full_version < '3.11'
# via
# anyio
@ -33,12 +33,12 @@ httpcore==1.0.9
# via httpx
httpx==0.28.1
# via
# netcheck
# netsleuth
# pytest-httpx
hyperframe==6.1.0
# via h2
icmplib==3.0.4
# via netcheck
# via netsleuth
idna==3.18
# via
# anyio
@ -54,15 +54,15 @@ packaging==26.3
pluggy==1.6.0
# via pytest
psutil==7.2.2
# via netcheck
# via netsleuth
pydantic==2.13.4
# via
# netcheck
# netsleuth
# pydantic-settings
pydantic-core==2.46.4
# via pydantic
pydantic-settings==2.15.0
# via netcheck
# via netsleuth
pygments==2.20.0
# via
# pytest
@ -76,17 +76,17 @@ pytest-httpx==0.36.2
python-dotenv==1.2.2
# via pydantic-settings
pyyaml==6.0.3
# via netcheck
# via netsleuth
rich==15.0.0
# via
# netcheck
# netsleuth
# typer
shellingham==1.5.4
# via typer
tomli==2.4.1 ; python_full_version < '3.11'
# via pytest
typer==0.27.1
# via netcheck
# via netsleuth
typing-extensions==4.16.0
# via
# anyio

View file

@ -1,4 +1,4 @@
from netcheck.cli import main
from netsleuth.cli import main
if __name__ == "__main__":
main()

View file

@ -5,8 +5,8 @@ from datetime import datetime, timedelta, timezone
import httpx
from netcheck.config import Providers
from netcheck.models import BgpEvent
from netsleuth.config import Providers
from netsleuth.models import BgpEvent
# These calls return the whole routing history of an ASN; on a large ISP that is
# tens of megabytes unless the window and row count are pinned.
@ -110,7 +110,7 @@ import time
from pathlib import Path
from typing import Awaitable, Callable
from netcheck.models import IxpPresence
from netsleuth.models import IxpPresence
def parse_asrank(payload: dict) -> tuple[int | None, int | None, int | None]:
@ -174,7 +174,7 @@ async def cached_json(
return payload
from netcheck.models import BgpIntel
from netsleuth.models import BgpIntel
def build_bgp_intel(

View file

@ -13,25 +13,25 @@ import httpx
import typer
from rich.console import Console
from netcheck import __version__
from netcheck.compare import diff_reports, load_report, render_diff
from netcheck.config import Settings, load_settings
from netcheck.exporter import build_report, egress_asn, render_markdown, report_filename, write_report
from netcheck.interpret import (
from netsleuth import __version__
from netsleuth.compare import diff_reports, load_report, render_diff
from netsleuth.config import Settings, load_settings
from netsleuth.exporter import build_report, egress_asn, render_markdown, report_filename, write_report
from netsleuth.interpret import (
assess_vpn,
gather_vpn_signals,
latency_findings,
path_findings,
speed_findings,
)
from netcheck.ip_geo import dual_stack_mismatch, gather_identity
from netcheck.models import Finding, IpGeo, LocalNet, ModuleResult, SpeedResult
from netcheck.netinfo import collect_local_net, detect_capabilities, is_tunnel_iface
from netcheck.orchestration import gather_modules, run_module, utc_now_iso
from netcheck.probes.dns_leak import collect_dns_leak
from netcheck.probes.latency import ping_fanout, tcp_connect_rtt
from netcheck.probes.traceroute import traceroute
from netcheck.speed import NDT7_CONSENT_NOTICE
from netsleuth.ip_geo import dual_stack_mismatch, gather_identity
from netsleuth.models import Finding, IpGeo, LocalNet, ModuleResult, SpeedResult
from netsleuth.netinfo import collect_local_net, detect_capabilities, is_tunnel_iface
from netsleuth.orchestration import gather_modules, run_module, utc_now_iso
from netsleuth.probes.dns_leak import collect_dns_leak
from netsleuth.probes.latency import ping_fanout, tcp_connect_rtt
from netsleuth.probes.traceroute import traceroute
from netsleuth.speed import NDT7_CONSENT_NOTICE
if sys.platform == "win32":
# A legacy console codepage (e.g. cp1251) cannot encode the emoji badges and
@ -150,7 +150,7 @@ async def _identity(client: httpx.AsyncClient, settings: Settings, options: Opti
async def _bgp_section(client, settings: Settings, asn: str | None, raw: dict):
if not asn:
return ModuleResult(name="bgp", status="skipped", warnings=["no ASN was resolved for this target"])
from netcheck.bgp import collect_bgp
from netsleuth.bgp import collect_bgp
key = settings.peeringdb_api_key.get_secret_value() if settings.peeringdb_api_key else None
intel, payloads = await collect_bgp(
@ -161,7 +161,7 @@ async def _bgp_section(client, settings: Settings, asn: str | None, raw: dict):
async def _reputation_section(client, settings: Settings, options: Options, geo: IpGeo, raw: dict):
from netcheck.reputation import (
from netsleuth.reputation import (
build_reputation,
fetch_abuseipdb,
fetch_internetdb,
@ -225,7 +225,7 @@ async def _traces(hosts, caps, settings: Settings, cycles: int, options: Options
async def _speed_section(client, settings: Settings, options: Options, idle_rtt_ms: float | None):
import shutil
from netcheck.speed import (
from netsleuth.speed import (
measure_with_bufferbloat,
run_speed_cascade,
tier_cloudflare,
@ -285,7 +285,7 @@ async def diagnose(settings: Settings, options: Options) -> tuple[dict, str, str
timeout=timeouts.http_seconds,
follow_redirects=True,
http2=True,
headers={"User-Agent": f"netcheck/{__version__}"},
headers={"User-Agent": f"netsleuth/{__version__}"},
) as client:
# Phase 1 — local facts and identity. Blocking: everything below needs the ASN.
modules["connection"] = await run_module(
@ -463,12 +463,12 @@ def run(
"if it cannot run, the cascade falls through to the normal tiers.[/yellow]"
)
if watch:
from netcheck.watch import run_watch
from netsleuth.watch import run_watch
asyncio.run(run_watch(settings, options))
raise typer.Exit(0)
console.print(f"netcheck {__version__} · {options.mode} mode · {platform.system()}")
console.print(f"netsleuth {__version__} · {options.mode} mode · {platform.system()}")
report, _markdown, _markdown_ru, json_path, md_path, ru_md_path = asyncio.run(diagnose(settings, options))
interpretation = report["interpretation"]
console.print(

View file

@ -5,7 +5,7 @@ from dataclasses import dataclass, field
from pathlib import Path
from typing import Any
from netcheck.exporter import badge
from netsleuth.exporter import badge
@dataclass
@ -124,7 +124,7 @@ def _block(title: str, changes: list[Change]) -> list[str]:
def render_diff(diff: ReportDiff, emoji: bool = True) -> str:
lines = ["# netcheck compare", ""]
lines = ["# netsleuth compare", ""]
lines += _block("Identity", diff.identity)
lines += _block("Latency", diff.latency)
lines += _block("Speed", diff.speed)

View file

@ -134,7 +134,7 @@ class Watch(BaseModel):
class Settings(BaseSettings):
model_config = SettingsConfigDict(
env_prefix="NETCHECK_",
env_prefix="NETSLEUTH_",
env_nested_delimiter="__",
env_file_encoding="utf-8",
extra="ignore",

View file

@ -6,8 +6,8 @@ import re
from pathlib import Path
from typing import Any
from netcheck.interpret import overall_verdict
from netcheck.models import Finding, ModuleResult, to_jsonable
from netsleuth.interpret import overall_verdict
from netsleuth.models import Finding, ModuleResult, to_jsonable
SCHEMA_VERSION = 1
SECTION_ORDER = (
@ -101,7 +101,7 @@ def write_report(
return json_path, md_path, ru_md_path
from netcheck.interpret import bufferbloat_consequence
from netsleuth.interpret import bufferbloat_consequence
SPARK_CHARS = "▁▂▃▅▇"
_BADGES = {"ok": "🟢", "info": "🟢", "warn": "🟡", "crit": "🔴"}
@ -210,7 +210,7 @@ def _header(report: dict, emoji: bool, lang: str = "en") -> list[str]:
summary = interp.get("summary_text", "") if lang == "en" else _ru_summary(report, status)
status_word = status if lang == "en" else _SEVERITY_WORDS_RU.get(status, status)
return [
"# netcheck report",
"# netsleuth report",
"",
f"- {_t(lang, 'Mode', 'Режим')}: `{meta.get('mode', 'auto')}`{target}",
f"- {_t(lang, 'Started', 'Начало')}: {meta.get('started_at', '?')} · "

View file

@ -2,8 +2,8 @@ from __future__ import annotations
from typing import Iterable
from netcheck.config import Band, BufferbloatBands, Thresholds, VpnBands
from netcheck.models import (
from netsleuth.config import Band, BufferbloatBands, Thresholds, VpnBands
from netsleuth.models import (
CfTrace,
DnsLeak,
Finding,
@ -15,7 +15,7 @@ from netcheck.models import (
TraceResult,
VpnAssessment,
)
from netcheck.netinfo import is_tunnel_iface, mtu_anomaly
from netsleuth.netinfo import is_tunnel_iface, mtu_anomaly
_SEVERITY_ORDER = {"ok": 0, "info": 1, "warn": 2, "crit": 3}

View file

@ -3,7 +3,7 @@ from __future__ import annotations
import ipaddress
import re
from netcheck.models import CfTrace, IpGeo
from netsleuth.models import CfTrace, IpGeo
_AS_PREFIX_RE = re.compile(r"^AS(\d+)\s*(?P<name>.*)$", re.IGNORECASE)
@ -214,7 +214,7 @@ import asyncio
import httpx
from netcheck.config import Providers
from netsleuth.config import Providers
async def _json(client: httpx.AsyncClient, url: str, **kwargs) -> dict:

View file

@ -12,7 +12,7 @@ import subprocess
import psutil
from netcheck.models import Capabilities, LocalNet
from netsleuth.models import Capabilities, LocalNet
_TUNNEL_PATTERNS = (
re.compile(r"^(tun|tap|utun|ppp|wg|nordlynx|proton|ipsec|gpd)\d*", re.IGNORECASE),

View file

@ -7,7 +7,7 @@ from typing import Any, Awaitable
import httpx
from netcheck.models import ModuleResult, ProbeError
from netsleuth.models import ModuleResult, ProbeError
_BLOCKED_STATUS = {401, 403, 451}
_RETRYABLE_STATUS_FLOOR = 500

View file

@ -2,7 +2,7 @@ from __future__ import annotations
import ipaddress
from netcheck.models import AdapterLeakResult, DnsLeak
from netsleuth.models import AdapterLeakResult, DnsLeak
_LEAK_NOTE = (
"This test only sees resolvers configured at the OS level. A browser using DoH "
@ -99,8 +99,8 @@ import asyncio
import dns.asyncresolver
import dns.exception
from netcheck.bgp import parse_cymru_origin
from netcheck.models import LocalNet
from netsleuth.bgp import parse_cymru_origin
from netsleuth.models import LocalNet
MYADDR_NAME = "o-o.myaddr.l.google.com"
AKAHELP_NAME = "whoami.ds.akahelp.net"

View file

@ -107,7 +107,7 @@ def _handle():
return iphlpapi, handle
def echo_once(dest: str, ttl: int, timeout_ms: int, payload: bytes = b"netcheck") -> IcmpReply:
def echo_once(dest: str, ttl: int, timeout_ms: int, payload: bytes = b"netsleuth") -> IcmpReply:
iphlpapi, handle = _handle()
try:
options = _IpOptionInformation(Ttl=ttl, Tos=0, Flags=0, OptionsSize=0, OptionsData=None)

View file

@ -4,9 +4,9 @@ import asyncio
import socket
import time
from netcheck.models import Capabilities, PingResult
from netcheck.netinfo import choose_latency_backend
from netcheck.stats import rtt_stats
from netsleuth.models import Capabilities, PingResult
from netsleuth.netinfo import choose_latency_backend
from netsleuth.stats import rtt_stats
def summarize_ping(
@ -57,7 +57,7 @@ def _resolve(host: str) -> str | None:
async def _icmp_samples(host: str, count: int, interval: float, timeout: float, backend: str) -> list[float | None]:
if backend == "icmp_win":
from netcheck.probes.icmp_win import ping_samples_win
from netsleuth.probes.icmp_win import ping_samples_win
return await asyncio.to_thread(ping_samples_win, host, count, interval, timeout)
from icmplib import async_ping

View file

@ -8,9 +8,9 @@ import shutil
import threading
from typing import Awaitable, Callable, TypeVar
from netcheck.models import Capabilities, TraceHop, TraceResult
from netcheck.probes.icmp_win import IcmpReply, classify_status
from netcheck.traceparse import build_trace_result, finalize_hop
from netsleuth.models import Capabilities, TraceHop, TraceResult
from netsleuth.probes.icmp_win import IcmpReply, classify_status
from netsleuth.traceparse import build_trace_result, finalize_hop
_T = TypeVar("_T")
@ -34,7 +34,7 @@ def _run_in_daemon_thread(func: Callable[..., _T], *args: object) -> "asyncio.Fu
else:
result.set_result(value)
threading.Thread(target=_target, name="netcheck-icmp-win", daemon=True).start()
threading.Thread(target=_target, name="netsleuth-icmp-win", daemon=True).start()
return asyncio.wrap_future(result)
@ -129,7 +129,7 @@ async def _tier_mtr(target: str, binary: str, cycles: int, max_hops: int, timeou
async def _tier_icmp_win(target: str, max_hops: int, timeout: float) -> TraceResult:
from netcheck.probes.icmp_win import trace_hops_win
from netsleuth.probes.icmp_win import trace_hops_win
replies = await _run_in_daemon_thread(trace_hops_win, target, max_hops, int(timeout * 1000))
hops = hops_from_win_replies(replies)

View file

@ -12,8 +12,8 @@ import dns.asyncresolver
import dns.exception
import httpx
from netcheck.config import Providers
from netcheck.models import DnsblHit, InternetDbResult, Reputation
from netsleuth.config import Providers
from netsleuth.models import DnsblHit, InternetDbResult, Reputation
def parse_netset(text: str) -> list[str]:

View file

@ -3,8 +3,8 @@ from __future__ import annotations
import re
from urllib.parse import parse_qs
from netcheck.models import CfL4Stats
from netcheck.stats import percentile
from netsleuth.models import CfL4Stats
from netsleuth.stats import percentile
_CFL4_RE = re.compile(r'cfL4\s*;\s*desc\s*=\s*"?\??(?P<query>[^",]*)"?')
@ -62,7 +62,7 @@ import asyncio
import time
from typing import Awaitable, Callable
from netcheck.models import SpeedResult, TierAttempt
from netsleuth.models import SpeedResult, TierAttempt
NDT7_CONSENT_NOTICE = (
"M-Lab NDT7 publishes every measurement as public CC0 open data, including your "
@ -106,7 +106,7 @@ import json
import httpx
from netcheck.config import Speedtest
from netsleuth.config import Speedtest
async def tier_ookla(binary: str, server: str | None, timeout: float) -> SpeedResult:
@ -213,9 +213,9 @@ async def tier_ndt7(client: httpx.AsyncClient, cfg: Speedtest, timeout: float) -
)
from netcheck.config import BufferbloatBands
from netcheck.interpret import grade_bufferbloat
from netcheck.stats import rtt_stats
from netsleuth.config import BufferbloatBands
from netsleuth.interpret import grade_bufferbloat
from netsleuth.stats import rtt_stats
async def probe_while(

View file

@ -2,8 +2,8 @@ from __future__ import annotations
import re
from netcheck.models import TraceHop, TraceResult
from netcheck.stats import rtt_stats
from netsleuth.models import TraceHop, TraceResult
from netsleuth.stats import rtt_stats
IPV4_RE = re.compile(r"\b(?:\d{1,3}\.){3}\d{1,3}\b")
IPV6_RE = re.compile(r"\b(?:[0-9a-fA-F]{0,4}:){2,7}[0-9a-fA-F]{0,4}\b")

View file

@ -11,14 +11,14 @@ from rich.console import Console
from rich.live import Live
from rich.table import Table
from netcheck import __version__
from netcheck.config import Settings
from netcheck.exporter import atomic_write, compact_timestamp, dump_json, sanitize_name, sparkline
from netcheck.ip_geo import gather_identity
from netcheck.models import PingResult, SpeedResult, to_jsonable
from netcheck.netinfo import detect_capabilities
from netcheck.orchestration import run_module, utc_now_iso
from netcheck.probes.latency import ping_fanout
from netsleuth import __version__
from netsleuth.config import Settings
from netsleuth.exporter import atomic_write, compact_timestamp, dump_json, sanitize_name, sparkline
from netsleuth.ip_geo import gather_identity
from netsleuth.models import PingResult, SpeedResult, to_jsonable
from netsleuth.netinfo import detect_capabilities
from netsleuth.orchestration import run_module, utc_now_iso
from netsleuth.probes.latency import ping_fanout
WATCH_SCHEMA_VERSION = 1
@ -112,7 +112,7 @@ def write_session(session: WatchSession, logs_dir: Path) -> Path:
def render_dashboard(session: WatchSession) -> Table:
table = Table(title=f"netcheck --watch · {session.asn or 'unknown ASN'} · cycle {len(session.cycles)}")
table = Table(title=f"netsleuth --watch · {session.asn or 'unknown ASN'} · cycle {len(session.cycles)}")
for column in ("Host", "Avg ms", "Jitter", "Loss", "Trend"):
table.add_column(column)
latest = session.cycles[-1]["hosts"] if session.cycles else {}
@ -137,7 +137,7 @@ def render_dashboard(session: WatchSession) -> Table:
async def run_watch(settings: Settings, options) -> Path:
# Imported here, not at module import time: cli imports watch lazily for --watch,
# and a module-level import back into cli would close the cycle.
from netcheck.cli import _speed_section
from netsleuth.cli import _speed_section
caps = detect_capabilities()
session = WatchSession(
@ -149,12 +149,12 @@ async def run_watch(settings: Settings, options) -> Path:
if options.extra_host:
hosts.append(("target-host", options.extra_host))
console = Console()
console.print(f"netcheck {__version__} · watching every {session.interval_seconds}s · Ctrl-C to stop")
console.print(f"netsleuth {__version__} · watching every {session.interval_seconds}s · Ctrl-C to stop")
async with httpx.AsyncClient(
timeout=settings.timeouts.http_seconds,
follow_redirects=True,
headers={"User-Agent": f"netcheck/{__version__}"},
headers={"User-Agent": f"netsleuth/{__version__}"},
) as client:
try:
geo, _cf, _flags, _raw = await gather_identity(client, settings.providers)

View file

@ -3,7 +3,7 @@ from __future__ import annotations
import httpx
import pytest
from netcheck.bgp import (
from netsleuth.bgp import (
classify_stability,
parse_announced_prefixes,
parse_as_overview,
@ -11,8 +11,8 @@ from netcheck.bgp import (
parse_bgp_updates,
ripestat,
)
from netcheck.config import Providers
from netcheck.models import BgpEvent
from netsleuth.config import Providers
from netsleuth.models import BgpEvent
def test_as_overview_extracts_holder_and_block(api_fixture):
@ -99,7 +99,7 @@ async def test_ripestat_does_not_bound_non_bulk_calls(httpx_mock):
import json
from pathlib import Path
from netcheck.bgp import (
from netsleuth.bgp import (
cached_json,
parse_asrank,
parse_cymru_origin,
@ -211,7 +211,7 @@ async def test_cached_json_survives_a_corrupt_cache_file(tmp_path: Path):
assert await cached_json(tmp_path, "k", ttl_hours=24, fetch=fetch) == {"value": "fresh"}
from netcheck.bgp import build_bgp_intel
from netsleuth.bgp import build_bgp_intel
def test_build_bgp_intel_merges_every_source(api_fixture):

View file

@ -2,7 +2,7 @@ from __future__ import annotations
import pytest
from netcheck.compare import (
from netsleuth.compare import (
Change,
diff_reports,
finding_changes,
@ -101,7 +101,7 @@ def test_diff_reports_assembles_every_part(before, after):
def test_render_diff_shows_before_and_after_values(before, after):
text = render_diff(diff_reports(before, after))
assert text.startswith("# netcheck compare")
assert text.startswith("# netsleuth compare")
assert "203.0.113.44" in text
assert "198.51.100.7" in text
assert "-188.2" in text

View file

@ -4,7 +4,7 @@ import textwrap
import pytest
from netcheck.config import load_settings
from netsleuth.config import load_settings
@pytest.fixture()
@ -48,15 +48,15 @@ def test_yaml_overrides_defaults(yaml_file, tmp_path):
def test_dotenv_overrides_yaml(yaml_file, tmp_path):
env_file = tmp_path / ".env"
env_file.write_text("NETCHECK_PROBING__PING_COUNT=13\n", encoding="utf-8")
env_file.write_text("NETSLEUTH_PROBING__PING_COUNT=13\n", encoding="utf-8")
s = load_settings(config_path=yaml_file, env_file=env_file)
assert s.probing.ping_count == 13
def test_environment_overrides_dotenv(yaml_file, tmp_path, monkeypatch):
env_file = tmp_path / ".env"
env_file.write_text("NETCHECK_PROBING__PING_COUNT=13\n", encoding="utf-8")
monkeypatch.setenv("NETCHECK_PROBING__PING_COUNT", "99")
env_file.write_text("NETSLEUTH_PROBING__PING_COUNT=13\n", encoding="utf-8")
monkeypatch.setenv("NETSLEUTH_PROBING__PING_COUNT", "99")
s = load_settings(config_path=yaml_file, env_file=env_file)
assert s.probing.ping_count == 99

View file

@ -1,6 +1,6 @@
from __future__ import annotations
from netcheck.probes.dns_leak import (
from netsleuth.probes.dns_leak import (
build_adapter_result,
build_dns_leak,
detect_ecs_leak,

View file

@ -2,7 +2,7 @@ from __future__ import annotations
import pytest
from netcheck.reputation import decode_dnsbl, reverse_ip, summarize_dnsbl
from netsleuth.reputation import decode_dnsbl, reverse_ip, summarize_dnsbl
def test_reverse_ip_builds_the_dnsbl_query_label():

View file

@ -6,7 +6,7 @@ from pathlib import Path
import pytest
from netcheck.exporter import (
from netsleuth.exporter import (
SCHEMA_VERSION,
SECTION_ORDER,
atomic_write,
@ -19,7 +19,7 @@ from netcheck.exporter import (
sanitize_name,
write_report,
)
from netcheck.models import (
from netsleuth.models import (
Finding,
IpGeo,
LocalNet,
@ -196,7 +196,7 @@ def test_egress_asn_is_read_out_of_the_assembled_report():
def test_write_report_names_the_file_after_the_target_in_target_mode(tmp_path: Path):
report = build_report({**meta(), "mode": "target", "target": "AS15169"}, modules(), [], {})
json_path, md_path, ru_md_path = write_report(
report, "# netcheck report\n", "# netcheck report\n", tmp_path
report, "# netsleuth report\n", "# netsleuth report\n", tmp_path
)
assert json_path.name == "report_AS15169_20260808T191200Z.json"
assert md_path.name == "report_AS15169_20260808T191200Z.md"
@ -221,18 +221,18 @@ def test_atomic_write_replaces_an_existing_file(tmp_path: Path):
def test_write_report_emits_both_artifacts_with_matching_names(tmp_path: Path):
report = build_report(meta(), modules(), [], {})
json_path, md_path, ru_md_path = write_report(
report, "# netcheck report\n", "# netcheck report\n", tmp_path
report, "# netsleuth report\n", "# netsleuth report\n", tmp_path
)
assert json_path.name == "report_AS64500_20260808T191200Z.json"
assert md_path.name == "report_AS64500_20260808T191200Z.md"
assert ru_md_path.name == "report_AS64500_20260808T191200Z.ru.md"
assert json.loads(json_path.read_text(encoding="utf-8"))["schema_version"] == SCHEMA_VERSION
assert md_path.read_text(encoding="utf-8").startswith("# netcheck report")
assert ru_md_path.read_text(encoding="utf-8").startswith("# netcheck report")
assert md_path.read_text(encoding="utf-8").startswith("# netsleuth report")
assert ru_md_path.read_text(encoding="utf-8").startswith("# netsleuth report")
from netcheck.exporter import SPARK_CHARS, badge, first_loss_jump, render_markdown, sparkline
from netcheck.models import (
from netsleuth.exporter import SPARK_CHARS, badge, first_loss_jump, render_markdown, sparkline
from netsleuth.models import (
AdapterLeakResult,
BgpIntel,
CfTrace,
@ -466,7 +466,7 @@ def test_markdown_has_every_section_from_the_spec_in_order():
"## Problems & recommendations",
"## Run diagnostics",
]
assert text.startswith("# netcheck report")
assert text.startswith("# netsleuth report")
def test_markdown_header_states_mode_timestamp_and_verdict():
@ -542,7 +542,7 @@ def test_markdown_without_emoji_uses_text_badges():
assert "🟡" not in text
from netcheck.exporter import unavailable
from netsleuth.exporter import unavailable
def dead_modules() -> dict[str, ModuleResult]:
@ -603,7 +603,7 @@ def test_unavailable_translates_the_skip_reason_into_russian():
def test_diagnostics_translates_module_warnings_into_russian():
from netcheck.exporter import _diagnostics
from netsleuth.exporter import _diagnostics
report = build_report(
meta(),
@ -666,7 +666,7 @@ def test_russian_markdown_is_non_empty_and_contains_cyrillic():
text = render_markdown(full_report(), lang="ru")
assert text.strip()
assert _has_cyrillic(text)
assert text.startswith("# netcheck report")
assert text.startswith("# netsleuth report")
def _structural_shape(text: str) -> tuple[int, int, int]:
@ -723,7 +723,7 @@ def test_render_markdown_default_call_is_byte_identical_to_before_the_lang_featu
"## Problems & recommendations",
"## Run diagnostics",
]
assert text.startswith("# netcheck report")
assert text.startswith("# netsleuth report")
assert "[Русский]" not in text
assert "[English]" not in text
assert "cloudflare-dns" in text

View file

@ -4,7 +4,7 @@ import struct
import pytest
from netcheck.probes.icmp_win import (
from netsleuth.probes.icmp_win import (
IP_DEST_HOST_UNREACHABLE,
IP_REQ_TIMED_OUT,
IP_SUCCESS,

View file

@ -3,7 +3,7 @@ from __future__ import annotations
import time
from unittest.mock import patch
from netcheck.probes.icmp_win import IP_REQ_TIMED_OUT, IcmpReply, trace_hops_win
from netsleuth.probes.icmp_win import IP_REQ_TIMED_OUT, IcmpReply, trace_hops_win
def _fake_gethostbyname(host: str) -> str:
@ -19,14 +19,14 @@ def test_trace_hops_win_total_elapsed_is_bounded_by_the_total_budget_not_hops_ti
# to the 100ms total budget.
call_timeouts: list[int] = []
def fake_echo_once(dest: str, ttl: int, timeout_ms: int, payload: bytes = b"netcheck") -> IcmpReply:
def fake_echo_once(dest: str, ttl: int, timeout_ms: int, payload: bytes = b"netsleuth") -> IcmpReply:
call_timeouts.append(timeout_ms)
time.sleep(timeout_ms / 1000.0)
return IcmpReply(address=None, status=IP_REQ_TIMED_OUT, rtt_ms=None, ttl=0)
with (
patch("netcheck.probes.icmp_win.socket.gethostbyname", side_effect=_fake_gethostbyname),
patch("netcheck.probes.icmp_win.echo_once", side_effect=fake_echo_once),
patch("netsleuth.probes.icmp_win.socket.gethostbyname", side_effect=_fake_gethostbyname),
patch("netsleuth.probes.icmp_win.echo_once", side_effect=fake_echo_once),
):
started = time.monotonic()
trace_hops_win("example.invalid", max_hops=30, timeout_ms=100)
@ -43,13 +43,13 @@ def test_trace_hops_win_total_elapsed_is_bounded_by_the_total_budget_not_hops_ti
def test_trace_hops_win_per_hop_timeout_is_capped_even_for_a_huge_total_budget():
call_timeouts: list[int] = []
def fake_echo_once(dest: str, ttl: int, timeout_ms: int, payload: bytes = b"netcheck") -> IcmpReply:
def fake_echo_once(dest: str, ttl: int, timeout_ms: int, payload: bytes = b"netsleuth") -> IcmpReply:
call_timeouts.append(timeout_ms)
return IcmpReply(address=None, status=IP_REQ_TIMED_OUT, rtt_ms=None, ttl=0)
with (
patch("netcheck.probes.icmp_win.socket.gethostbyname", side_effect=_fake_gethostbyname),
patch("netcheck.probes.icmp_win.echo_once", side_effect=fake_echo_once),
patch("netsleuth.probes.icmp_win.socket.gethostbyname", side_effect=_fake_gethostbyname),
patch("netsleuth.probes.icmp_win.echo_once", side_effect=fake_echo_once),
):
# A single-hop trace with a huge total budget (e.g. subprocess_seconds=60s
# divided across a small max_hops) must still not hand the whole budget
@ -60,16 +60,16 @@ def test_trace_hops_win_per_hop_timeout_is_capped_even_for_a_huge_total_budget()
def test_trace_hops_win_happy_path_hop_count_and_ok_break_are_unchanged():
from netcheck.probes.icmp_win import IP_SUCCESS
from netsleuth.probes.icmp_win import IP_SUCCESS
def fake_echo_once(dest: str, ttl: int, timeout_ms: int, payload: bytes = b"netcheck") -> IcmpReply:
def fake_echo_once(dest: str, ttl: int, timeout_ms: int, payload: bytes = b"netsleuth") -> IcmpReply:
if ttl < 3:
return IcmpReply(address=f"10.0.0.{ttl}", status=IP_REQ_TIMED_OUT, rtt_ms=None, ttl=0)
return IcmpReply(address="203.0.113.1", status=IP_SUCCESS, rtt_ms=5.0, ttl=64)
with (
patch("netcheck.probes.icmp_win.socket.gethostbyname", side_effect=_fake_gethostbyname),
patch("netcheck.probes.icmp_win.echo_once", side_effect=fake_echo_once),
patch("netsleuth.probes.icmp_win.socket.gethostbyname", side_effect=_fake_gethostbyname),
patch("netsleuth.probes.icmp_win.echo_once", side_effect=fake_echo_once),
):
hops = trace_hops_win("example.invalid", max_hops=30, timeout_ms=60_000)

View file

@ -2,9 +2,9 @@ from __future__ import annotations
import pytest
from netcheck.config import Band, Thresholds
from netcheck.models import PingResult, TraceHop, TraceResult
from netcheck.interpret import latency_findings, path_findings, severity_for, worst
from netsleuth.config import Band, Thresholds
from netsleuth.models import PingResult, TraceHop, TraceResult
from netsleuth.interpret import latency_findings, path_findings, severity_for, worst
@pytest.fixture()
@ -156,9 +156,9 @@ def test_path_findings_report_an_incomplete_trace():
assert [f.id for f in path_findings(trace)] == ["path.incomplete"]
from netcheck.config import VpnBands
from netcheck.models import AdapterLeakResult, CfTrace, DnsLeak, IpGeo, LocalNet, Signal
from netcheck.interpret import SIGNAL_WEIGHTS, assess_vpn, gather_vpn_signals, score_vpn
from netsleuth.config import VpnBands
from netsleuth.models import AdapterLeakResult, CfTrace, DnsLeak, IpGeo, LocalNet, Signal
from netsleuth.interpret import SIGNAL_WEIGHTS, assess_vpn, gather_vpn_signals, score_vpn
def sig(name: str, observed: bool = True) -> Signal:
@ -303,9 +303,9 @@ def test_assess_vpn_returns_a_complete_assessment():
assert assessment.confidence >= 0.75
from netcheck.config import BufferbloatBands
from netcheck.models import Finding, SpeedResult
from netcheck.interpret import bufferbloat_consequence, grade_bufferbloat, overall_verdict, speed_findings
from netsleuth.config import BufferbloatBands
from netsleuth.models import Finding, SpeedResult
from netsleuth.interpret import bufferbloat_consequence, grade_bufferbloat, overall_verdict, speed_findings
def test_bufferbloat_grades_follow_the_configured_bands():

View file

@ -3,8 +3,8 @@ from __future__ import annotations
import httpx
import pytest
from netcheck.config import Providers
from netcheck.ip_geo import (
from netsleuth.config import Providers
from netsleuth.ip_geo import (
classify_ip_type,
gather_identity,
normalize_freeipapi,
@ -130,8 +130,8 @@ def test_ip_type_classification(mobile, proxy, hosting, known, expected):
assert classify_ip_type(mobile, proxy, hosting, known) == expected
from netcheck.models import IpGeo
from netcheck.ip_geo import dual_stack_mismatch, merge_geo
from netsleuth.models import IpGeo
from netsleuth.ip_geo import dual_stack_mismatch, merge_geo
def test_merge_takes_the_first_non_empty_value_in_priority_order():

View file

@ -4,7 +4,7 @@ import json
import pytest
from netcheck.models import (
from netsleuth.models import (
AdapterLeakResult,
BgpEvent,
BgpIntel,

View file

@ -1,7 +1,7 @@
from __future__ import annotations
from netcheck.models import Capabilities
from netcheck.netinfo import choose_latency_backend, choose_trace_backend, degradation_note
from netsleuth.models import Capabilities
from netsleuth.netinfo import choose_latency_backend, choose_trace_backend, degradation_note
def caps(**kw) -> Capabilities:
@ -69,7 +69,7 @@ def test_degradation_note_on_windows_mentions_the_api_rather_than_sysctl():
import pytest
from netcheck.netinfo import iface_for_ip, is_tunnel_iface, mtu_anomaly
from netsleuth.netinfo import iface_for_ip, is_tunnel_iface, mtu_anomaly
def test_iface_for_ip_matches_the_owning_adapter():

View file

@ -5,8 +5,8 @@ import asyncio
import httpx
import pytest
from netcheck.models import ModuleResult, ProbeError
from netcheck.orchestration import classify_exception, gather_modules, run_module
from netsleuth.models import ModuleResult, ProbeError
from netsleuth.orchestration import classify_exception, gather_modules, run_module
def test_classify_timeout_errors():

View file

@ -2,7 +2,7 @@ from __future__ import annotations
import pytest
from netcheck.probes.latency import summarize_ping
from netsleuth.probes.latency import summarize_ping
def test_summarize_builds_a_complete_ping_result():

View file

@ -1,6 +1,6 @@
from __future__ import annotations
from netcheck.reputation import (
from netsleuth.reputation import (
NetsetIndex,
build_reputation,
captcha_risk,

View file

@ -2,7 +2,7 @@ from __future__ import annotations
import pytest
from netcheck.speed import bufferbloat_delta, mbps, parse_server_timing_cfl4, throughput_from_samples
from netsleuth.speed import bufferbloat_delta, mbps, parse_server_timing_cfl4, throughput_from_samples
def test_mbps_converts_bytes_and_seconds():
@ -86,8 +86,8 @@ def test_bufferbloat_delta_needs_both_a_baseline_and_samples():
assert bufferbloat_delta(12.0, []) is None
from netcheck.models import SpeedResult, TierAttempt
from netcheck.speed import NDT7_CONSENT_NOTICE, run_speed_cascade
from netsleuth.models import SpeedResult, TierAttempt
from netsleuth.speed import NDT7_CONSENT_NOTICE, run_speed_cascade
async def test_cascade_stops_at_the_first_tier_that_returns_a_download_figure():
@ -171,8 +171,8 @@ def test_the_ndt7_consent_notice_states_what_gets_published():
assert "public" in NDT7_CONSENT_NOTICE.lower()
from netcheck.config import BufferbloatBands
from netcheck.speed import measure_with_bufferbloat, probe_while
from netsleuth.config import BufferbloatBands
from netsleuth.speed import measure_with_bufferbloat, probe_while
async def test_probe_while_collects_samples_for_the_whole_duration_of_the_work():

View file

@ -4,7 +4,7 @@ import math
import pytest
from netcheck.stats import percentile, rtt_stats
from netsleuth.stats import percentile, rtt_stats
def test_all_samples_present():

View file

@ -2,7 +2,7 @@ from __future__ import annotations
import pytest
from netcheck.traceparse import parse_linux
from netsleuth.traceparse import parse_linux
@pytest.fixture()
@ -73,7 +73,7 @@ def test_linux_parser_tolerates_empty_and_garbage_input():
assert parse_linux("bash: traceroute: command not found\n") == []
from netcheck.traceparse import WINDOWS_SUB_MS, parse_windows
from netsleuth.traceparse import WINDOWS_SUB_MS, parse_windows
@pytest.fixture()
@ -143,7 +143,7 @@ def test_windows_parser_tolerates_empty_input():
assert parse_windows("") == []
from netcheck.traceparse import parse_darwin
from netsleuth.traceparse import parse_darwin
def test_darwin_basic_hops_match_the_gnu_shape(trace_fixture):
@ -185,7 +185,7 @@ def test_darwin_parser_tolerates_empty_input():
assert parse_darwin("") == []
from netcheck.traceparse import build_trace_result, parse_traceroute
from netsleuth.traceparse import build_trace_result, parse_traceroute
def test_dispatcher_routes_by_os_name(trace_fixture):

View file

@ -5,9 +5,9 @@ import threading
import pytest
from netcheck.models import Capabilities, TraceHop, TraceResult
from netcheck.probes.icmp_win import IP_REQ_TIMED_OUT, IP_SUCCESS, IP_TTL_EXPIRED_TRANSIT, IcmpReply
from netcheck.probes.traceroute import _run_in_daemon_thread, hops_from_win_replies, run_cascade, tier_order
from netsleuth.models import Capabilities, TraceHop, TraceResult
from netsleuth.probes.icmp_win import IP_REQ_TIMED_OUT, IP_SUCCESS, IP_TTL_EXPIRED_TRANSIT, IcmpReply
from netsleuth.probes.traceroute import _run_in_daemon_thread, hops_from_win_replies, run_cascade, tier_order
def caps(**kw) -> Capabilities:

View file

@ -5,8 +5,8 @@ from pathlib import Path
import pytest
from netcheck.models import PingResult, SpeedResult
from netcheck.watch import (
from netsleuth.models import PingResult, SpeedResult
from netsleuth.watch import (
WATCH_SCHEMA_VERSION,
WatchSession,
is_speedtest_cycle,

2
uv.lock generated
View file

@ -208,7 +208,7 @@ wheels = [
]
[[package]]
name = "netcheck"
name = "netsleuth"
version = "0.1.0"
source = { editable = "." }
dependencies = [