From 413baa9cd4a0b4639dd062c7ef9fc643e185e28f Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Wed, 26 Aug 2026 14:08:43 +0000 Subject: [PATCH] refactor: remove unused `timeframe_days` variable from `BgpContext` `timeframe_days` is configuration logic that was incorrectly placed into `BgpContext`, which is meant for raw data from BGP sources. This refactor removes it from `BgpContext` and instead passes it as a parameter to `build_bgp_intel` where it's actually used by `classify_stability`. Co-authored-by: Horizontal42 <75383519+Horizontal42@users.noreply.github.com> --- src/netsleuth/bgp.py | 35 ++++++++++++++++-------- tests/test_bgp.py | 63 ++++++++++++++++++++++++++++---------------- 2 files changed, 64 insertions(+), 34 deletions(-) diff --git a/src/netsleuth/bgp.py b/src/netsleuth/bgp.py index ad88c2e..b0a1dff 100644 --- a/src/netsleuth/bgp.py +++ b/src/netsleuth/bgp.py @@ -84,7 +84,9 @@ def classify_stability(events: list[BgpEvent], days: int) -> str: if days <= 0: return "unknown" withdrawals = sum(1 for e in events if e.type == "W") - return "unstable" if withdrawals / days >= _UNSTABLE_WITHDRAWALS_PER_DAY else "stable" + return ( + "unstable" if withdrawals / days >= _UNSTABLE_WITHDRAWALS_PER_DAY else "stable" + ) async def ripestat( @@ -99,7 +101,9 @@ async def ripestat( start = datetime.now(UTC) - timedelta(days=providers.ripestat_timeframe_days) params["max_rows"] = str(providers.ripestat_max_rows) params["starttime"] = start.strftime("%Y-%m-%dT%H:%M:%S") - response = await client.get(f"{providers.ripestat_base_url}/{call}/data.json", params=params) + response = await client.get( + f"{providers.ripestat_base_url}/{call}/data.json", params=params + ) response.raise_for_status() return response.json() @@ -188,16 +192,19 @@ class BgpContext: asrank: dict | None pdb_net: dict | None pdb_ixlan: dict | None - timeframe_days: int -def build_bgp_intel(ctx: BgpContext) -> BgpIntel: +def build_bgp_intel(ctx: BgpContext, timeframe_days: int) -> BgpIntel: info = parse_as_overview(ctx.overview) upstreams, peers, downstreams = parse_asn_neighbours(ctx.neighbours) announced, v4, v6 = parse_announced_prefixes(ctx.prefixes) events = parse_bgp_updates(ctx.updates) - rank, cone_asns, cone_prefixes = parse_asrank(ctx.asrank) if ctx.asrank else (None, None, None) - info_type, traffic, _net_id = parse_peeringdb_net(ctx.pdb_net) if ctx.pdb_net else (None, None, None) + rank, cone_asns, cone_prefixes = ( + parse_asrank(ctx.asrank) if ctx.asrank else (None, None, None) + ) + info_type, traffic, _net_id = ( + parse_peeringdb_net(ctx.pdb_net) if ctx.pdb_net else (None, None, None) + ) return BgpIntel( asn=ctx.asn, holder=info["holder"], @@ -210,7 +217,7 @@ def build_bgp_intel(ctx: BgpContext) -> BgpIntel: prefix_count_v4=v4, prefix_count_v6=v6, flaps=events, - stability=classify_stability(events, ctx.timeframe_days), + stability=classify_stability(events, timeframe_days), ixps=parse_peeringdb_netixlan(ctx.pdb_ixlan) if ctx.pdb_ixlan else [], pdb_info_type=info_type, pdb_traffic=traffic, @@ -247,7 +254,8 @@ async def collect_bgp( async def _asrank() -> dict: response = await client.post( - providers.asrank_url, json={"query": _ASRANK_QUERY, "variables": {"asn": number}} + providers.asrank_url, + json={"query": _ASRANK_QUERY, "variables": {"asn": number}}, ) response.raise_for_status() return response.json() @@ -266,7 +274,12 @@ async def collect_bgp( ripestat(client, providers, "announced-prefixes", asn), ripestat(client, providers, "bgp-updates", asn), _asrank(), - cached_json(cache_dir, f"pdb-net-{number}", providers.peeringdb_cache_hours, lambda: _pdb("net", asn=number)), + cached_json( + cache_dir, + f"pdb-net-{number}", + providers.peeringdb_cache_hours, + lambda: _pdb("net", asn=number), + ), return_exceptions=True, ) overview, neighbours, prefixes, updates, asrank, pdb_net = [ @@ -297,8 +310,8 @@ async def collect_bgp( asrank=asrank, pdb_net=pdb_net, pdb_ixlan=pdb_ixlan, - timeframe_days=providers.ripestat_timeframe_days, - ) + ), + timeframe_days=providers.ripestat_timeframe_days, ) raw = { "ripestat-as-overview": overview, diff --git a/tests/test_bgp.py b/tests/test_bgp.py index 202fddc..4873c00 100644 --- a/tests/test_bgp.py +++ b/tests/test_bgp.py @@ -3,11 +3,18 @@ from __future__ import annotations import httpx from netsleuth.bgp import ( + BgpContext, + build_bgp_intel, + cached_json, classify_stability, parse_announced_prefixes, parse_as_overview, parse_asn_neighbours, + parse_asrank, parse_bgp_updates, + parse_cymru_origin, + parse_peeringdb_net, + parse_peeringdb_netixlan, ripestat, ) from netsleuth.config import Providers @@ -29,14 +36,18 @@ def test_as_overview_of_an_error_payload_is_empty(): def test_neighbours_split_into_upstreams_peers_and_downstreams(api_fixture): - upstreams, peers, downstreams = parse_asn_neighbours(api_fixture("ripestat_asn_neighbours.json")) + upstreams, peers, downstreams = parse_asn_neighbours( + api_fixture("ripestat_asn_neighbours.json") + ) assert upstreams == ["AS3356", "AS1299"] assert downstreams == ["AS6939", "AS64501"] assert peers == ["AS8075"] def test_announced_prefixes_are_counted_by_family(api_fixture): - prefixes, v4, v6 = parse_announced_prefixes(api_fixture("ripestat_announced_prefixes.json")) + prefixes, v4, v6 = parse_announced_prefixes( + api_fixture("ripestat_announced_prefixes.json") + ) assert prefixes == ["203.0.113.0/24", "198.51.100.0/24", "2001:db8::/32"] assert v4 == 2 assert v6 == 1 @@ -61,12 +72,18 @@ def test_stability_of_a_quiet_asn_is_stable(): def test_stability_of_a_flapping_asn_is_unstable(): - events = [BgpEvent(timestamp="2026-08-01T00:00:00", type="W", prefix="203.0.113.0/24") for _ in range(60)] + events = [ + BgpEvent(timestamp="2026-08-01T00:00:00", type="W", prefix="203.0.113.0/24") + for _ in range(60) + ] assert classify_stability(events, days=14) == "unstable" def test_stability_only_counts_withdrawals_not_announcements(): - events = [BgpEvent(timestamp="2026-08-01T00:00:00", type="A", prefix="203.0.113.0/24") for _ in range(200)] + events = [ + BgpEvent(timestamp="2026-08-01T00:00:00", type="A", prefix="203.0.113.0/24") + for _ in range(200) + ] assert classify_stability(events, days=14) == "stable" @@ -98,14 +115,6 @@ async def test_ripestat_does_not_bound_non_bulk_calls(httpx_mock): import json from pathlib import Path -from netsleuth.bgp import ( - cached_json, - parse_asrank, - parse_cymru_origin, - parse_peeringdb_net, - parse_peeringdb_netixlan, -) - def test_asrank_gives_rank_and_customer_cone(api_fixture): rank, cone_asns, cone_prefixes = parse_asrank(api_fixture("asrank.json")) @@ -163,12 +172,21 @@ async def test_cached_json_writes_then_reuses_the_cache(tmp_path: Path): calls.append(1) return {"value": len(calls)} - first = await cached_json(tmp_path, "peeringdb-net-64500", ttl_hours=24, fetch=fetch) - second = await cached_json(tmp_path, "peeringdb-net-64500", ttl_hours=24, fetch=fetch) + first = await cached_json( + tmp_path, "peeringdb-net-64500", ttl_hours=24, fetch=fetch + ) + second = await cached_json( + tmp_path, "peeringdb-net-64500", ttl_hours=24, fetch=fetch + ) assert first == {"value": 1} assert second == {"value": 1} assert len(calls) == 1 - assert json.loads((tmp_path / "peeringdb-net-64500.json").read_text(encoding="utf-8"))["value"] == 1 + assert ( + json.loads((tmp_path / "peeringdb-net-64500.json").read_text(encoding="utf-8"))[ + "value" + ] + == 1 + ) async def test_cached_json_refetches_once_the_entry_expires(tmp_path: Path): @@ -207,10 +225,9 @@ async def test_cached_json_survives_a_corrupt_cache_file(tmp_path: Path): async def fetch() -> dict: return {"value": "fresh"} - assert await cached_json(tmp_path, "k", ttl_hours=24, fetch=fetch) == {"value": "fresh"} - - -from netsleuth.bgp import BgpContext, build_bgp_intel + assert await cached_json(tmp_path, "k", ttl_hours=24, fetch=fetch) == { + "value": "fresh" + } def test_build_bgp_intel_merges_every_source(api_fixture): @@ -224,8 +241,8 @@ def test_build_bgp_intel_merges_every_source(api_fixture): asrank=api_fixture("asrank.json"), pdb_net=api_fixture("peeringdb_net.json"), pdb_ixlan=api_fixture("peeringdb_netixlan.json"), - timeframe_days=14, - ) + ), + timeframe_days=14, ) assert intel.asn == "AS64500" assert intel.holder == "EXAMPLE-AS Example Telecom BV" @@ -251,8 +268,8 @@ def test_build_bgp_intel_tolerates_every_optional_source_being_absent(api_fixtur asrank=None, pdb_net=None, pdb_ixlan=None, - timeframe_days=14, - ) + ), + timeframe_days=14, ) assert intel.asn == "AS64500" assert intel.upstreams == []