diff --git a/pyproject.toml b/pyproject.toml index 07bacd2..a0865ed 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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.*"] diff --git a/requirements.txt b/requirements.txt index 8c2c2d5..3359fb4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -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 diff --git a/src/netcheck/__init__.py b/src/netsleuth/__init__.py similarity index 100% rename from src/netcheck/__init__.py rename to src/netsleuth/__init__.py diff --git a/src/netcheck/__main__.py b/src/netsleuth/__main__.py similarity index 55% rename from src/netcheck/__main__.py rename to src/netsleuth/__main__.py index 8aad20e..95f73ba 100644 --- a/src/netcheck/__main__.py +++ b/src/netsleuth/__main__.py @@ -1,4 +1,4 @@ -from netcheck.cli import main +from netsleuth.cli import main if __name__ == "__main__": main() diff --git a/src/netcheck/bgp.py b/src/netsleuth/bgp.py similarity index 98% rename from src/netcheck/bgp.py rename to src/netsleuth/bgp.py index 7b71302..3aebeff 100644 --- a/src/netcheck/bgp.py +++ b/src/netsleuth/bgp.py @@ -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( diff --git a/src/netcheck/cli.py b/src/netsleuth/cli.py similarity index 94% rename from src/netcheck/cli.py rename to src/netsleuth/cli.py index ded8d77..c765c15 100644 --- a/src/netcheck/cli.py +++ b/src/netsleuth/cli.py @@ -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( diff --git a/src/netcheck/compare.py b/src/netsleuth/compare.py similarity index 98% rename from src/netcheck/compare.py rename to src/netsleuth/compare.py index 7dd80d4..755952e 100644 --- a/src/netcheck/compare.py +++ b/src/netsleuth/compare.py @@ -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) diff --git a/src/netcheck/config.py b/src/netsleuth/config.py similarity index 99% rename from src/netcheck/config.py rename to src/netsleuth/config.py index 0c8d852..a7f2d7b 100644 --- a/src/netcheck/config.py +++ b/src/netsleuth/config.py @@ -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", diff --git a/src/netcheck/exporter.py b/src/netsleuth/exporter.py similarity index 99% rename from src/netcheck/exporter.py rename to src/netsleuth/exporter.py index 4af672d..8ea7c59 100644 --- a/src/netcheck/exporter.py +++ b/src/netsleuth/exporter.py @@ -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', '?')} · " diff --git a/src/netcheck/interpret.py b/src/netsleuth/interpret.py similarity index 99% rename from src/netcheck/interpret.py rename to src/netsleuth/interpret.py index 4f8d5b0..a4d1ac4 100644 --- a/src/netcheck/interpret.py +++ b/src/netsleuth/interpret.py @@ -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} diff --git a/src/netcheck/ip_geo.py b/src/netsleuth/ip_geo.py similarity index 99% rename from src/netcheck/ip_geo.py rename to src/netsleuth/ip_geo.py index edf2272..53e5207 100644 --- a/src/netcheck/ip_geo.py +++ b/src/netsleuth/ip_geo.py @@ -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.*)$", 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: diff --git a/src/netcheck/models.py b/src/netsleuth/models.py similarity index 100% rename from src/netcheck/models.py rename to src/netsleuth/models.py diff --git a/src/netcheck/netinfo.py b/src/netsleuth/netinfo.py similarity index 99% rename from src/netcheck/netinfo.py rename to src/netsleuth/netinfo.py index 633160e..84e16f9 100644 --- a/src/netcheck/netinfo.py +++ b/src/netsleuth/netinfo.py @@ -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), diff --git a/src/netcheck/orchestration.py b/src/netsleuth/orchestration.py similarity index 98% rename from src/netcheck/orchestration.py rename to src/netsleuth/orchestration.py index f4b3e8a..cc2a517 100644 --- a/src/netcheck/orchestration.py +++ b/src/netsleuth/orchestration.py @@ -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 diff --git a/src/netcheck/probes/__init__.py b/src/netsleuth/probes/__init__.py similarity index 100% rename from src/netcheck/probes/__init__.py rename to src/netsleuth/probes/__init__.py diff --git a/src/netcheck/probes/dns_leak.py b/src/netsleuth/probes/dns_leak.py similarity index 97% rename from src/netcheck/probes/dns_leak.py rename to src/netsleuth/probes/dns_leak.py index c388293..a594a3c 100644 --- a/src/netcheck/probes/dns_leak.py +++ b/src/netsleuth/probes/dns_leak.py @@ -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" diff --git a/src/netcheck/probes/icmp_win.py b/src/netsleuth/probes/icmp_win.py similarity index 99% rename from src/netcheck/probes/icmp_win.py rename to src/netsleuth/probes/icmp_win.py index c5e2b5c..edc9ae9 100644 --- a/src/netcheck/probes/icmp_win.py +++ b/src/netsleuth/probes/icmp_win.py @@ -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) diff --git a/src/netcheck/probes/latency.py b/src/netsleuth/probes/latency.py similarity index 93% rename from src/netcheck/probes/latency.py rename to src/netsleuth/probes/latency.py index c719e0b..7d46d5e 100644 --- a/src/netcheck/probes/latency.py +++ b/src/netsleuth/probes/latency.py @@ -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 diff --git a/src/netcheck/probes/traceroute.py b/src/netsleuth/probes/traceroute.py similarity index 95% rename from src/netcheck/probes/traceroute.py rename to src/netsleuth/probes/traceroute.py index ddfbcf9..9da8e45 100644 --- a/src/netcheck/probes/traceroute.py +++ b/src/netsleuth/probes/traceroute.py @@ -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) diff --git a/src/netcheck/reputation.py b/src/netsleuth/reputation.py similarity index 98% rename from src/netcheck/reputation.py rename to src/netsleuth/reputation.py index 503bbec..d838566 100644 --- a/src/netcheck/reputation.py +++ b/src/netsleuth/reputation.py @@ -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]: diff --git a/src/netcheck/speed.py b/src/netsleuth/speed.py similarity index 96% rename from src/netcheck/speed.py rename to src/netsleuth/speed.py index 77e34a1..89176fa 100644 --- a/src/netcheck/speed.py +++ b/src/netsleuth/speed.py @@ -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[^",]*)"?') @@ -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( diff --git a/src/netcheck/stats.py b/src/netsleuth/stats.py similarity index 100% rename from src/netcheck/stats.py rename to src/netsleuth/stats.py diff --git a/src/netcheck/traceparse.py b/src/netsleuth/traceparse.py similarity index 98% rename from src/netcheck/traceparse.py rename to src/netsleuth/traceparse.py index 8cd0223..5065fff 100644 --- a/src/netcheck/traceparse.py +++ b/src/netsleuth/traceparse.py @@ -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") diff --git a/src/netcheck/watch.py b/src/netsleuth/watch.py similarity index 89% rename from src/netcheck/watch.py rename to src/netsleuth/watch.py index a7fb556..d1c6b59 100644 --- a/src/netcheck/watch.py +++ b/src/netsleuth/watch.py @@ -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) diff --git a/tests/test_bgp.py b/tests/test_bgp.py index 362e95e..d0cda42 100644 --- a/tests/test_bgp.py +++ b/tests/test_bgp.py @@ -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): diff --git a/tests/test_compare.py b/tests/test_compare.py index f1b0ec5..0f1fc5d 100644 --- a/tests/test_compare.py +++ b/tests/test_compare.py @@ -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 diff --git a/tests/test_config.py b/tests/test_config.py index 38f7ab7..05ef534 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -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 diff --git a/tests/test_dns_leak.py b/tests/test_dns_leak.py index 43218e9..7a45485 100644 --- a/tests/test_dns_leak.py +++ b/tests/test_dns_leak.py @@ -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, diff --git a/tests/test_dnsbl_decode.py b/tests/test_dnsbl_decode.py index cb7d29d..cf45445 100644 --- a/tests/test_dnsbl_decode.py +++ b/tests/test_dnsbl_decode.py @@ -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(): diff --git a/tests/test_exporter.py b/tests/test_exporter.py index f1672b1..641558c 100644 --- a/tests/test_exporter.py +++ b/tests/test_exporter.py @@ -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 diff --git a/tests/test_icmp_win_parse.py b/tests/test_icmp_win_parse.py index 52ebbf1..9827345 100644 --- a/tests/test_icmp_win_parse.py +++ b/tests/test_icmp_win_parse.py @@ -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, diff --git a/tests/test_icmp_win_trace.py b/tests/test_icmp_win_trace.py index 5909fd5..4768dae 100644 --- a/tests/test_icmp_win_trace.py +++ b/tests/test_icmp_win_trace.py @@ -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) diff --git a/tests/test_interpret.py b/tests/test_interpret.py index 3c798b6..bd4fe73 100644 --- a/tests/test_interpret.py +++ b/tests/test_interpret.py @@ -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(): diff --git a/tests/test_ip_geo_normalize.py b/tests/test_ip_geo_normalize.py index 6de54d7..36e6253 100644 --- a/tests/test_ip_geo_normalize.py +++ b/tests/test_ip_geo_normalize.py @@ -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(): diff --git a/tests/test_models.py b/tests/test_models.py index 73740aa..73bd8c2 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -4,7 +4,7 @@ import json import pytest -from netcheck.models import ( +from netsleuth.models import ( AdapterLeakResult, BgpEvent, BgpIntel, diff --git a/tests/test_netinfo.py b/tests/test_netinfo.py index d14cc4e..e1a349f 100644 --- a/tests/test_netinfo.py +++ b/tests/test_netinfo.py @@ -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(): diff --git a/tests/test_orchestration.py b/tests/test_orchestration.py index 7ff10bb..8cf95a3 100644 --- a/tests/test_orchestration.py +++ b/tests/test_orchestration.py @@ -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(): diff --git a/tests/test_prober_stats.py b/tests/test_prober_stats.py index 2a1bf09..f66f35b 100644 --- a/tests/test_prober_stats.py +++ b/tests/test_prober_stats.py @@ -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(): diff --git a/tests/test_reputation.py b/tests/test_reputation.py index 16920c9..8d9592e 100644 --- a/tests/test_reputation.py +++ b/tests/test_reputation.py @@ -1,6 +1,6 @@ from __future__ import annotations -from netcheck.reputation import ( +from netsleuth.reputation import ( NetsetIndex, build_reputation, captcha_risk, diff --git a/tests/test_speed_math.py b/tests/test_speed_math.py index ae2fb88..0909a44 100644 --- a/tests/test_speed_math.py +++ b/tests/test_speed_math.py @@ -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(): diff --git a/tests/test_stats.py b/tests/test_stats.py index 2557a16..5f0a0a4 100644 --- a/tests/test_stats.py +++ b/tests/test_stats.py @@ -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(): diff --git a/tests/test_traceparse.py b/tests/test_traceparse.py index d94713d..283f0fa 100644 --- a/tests/test_traceparse.py +++ b/tests/test_traceparse.py @@ -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): diff --git a/tests/test_traceroute_cascade.py b/tests/test_traceroute_cascade.py index 3a747f9..5cdf059 100644 --- a/tests/test_traceroute_cascade.py +++ b/tests/test_traceroute_cascade.py @@ -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: diff --git a/tests/test_watch.py b/tests/test_watch.py index a215318..e92caba 100644 --- a/tests/test_watch.py +++ b/tests/test_watch.py @@ -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, diff --git a/uv.lock b/uv.lock index ce02e13..cf4bf8b 100644 --- a/uv.lock +++ b/uv.lock @@ -208,7 +208,7 @@ wheels = [ ] [[package]] -name = "netcheck" +name = "netsleuth" version = "0.1.0" source = { editable = "." } dependencies = [