pybgpkit 0.7.0__tar.gz → 0.9.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {pybgpkit-0.7.0/pybgpkit.egg-info → pybgpkit-0.9.0}/PKG-INFO +2 -2
- {pybgpkit-0.7.0 → pybgpkit-0.9.0}/bgpkit/__init__.py +1 -0
- pybgpkit-0.9.0/bgpkit/_utils.py +39 -0
- pybgpkit-0.9.0/bgpkit/bgpkit_asn.py +116 -0
- pybgpkit-0.9.0/bgpkit/bgpkit_broker.py +202 -0
- pybgpkit-0.9.0/bgpkit/bgpkit_community.py +90 -0
- pybgpkit-0.9.0/bgpkit/bgpkit_roas.py +85 -0
- {pybgpkit-0.7.0 → pybgpkit-0.9.0/pybgpkit.egg-info}/PKG-INFO +2 -2
- {pybgpkit-0.7.0 → pybgpkit-0.9.0}/pybgpkit.egg-info/SOURCES.txt +7 -1
- pybgpkit-0.9.0/pybgpkit.egg-info/requires.txt +5 -0
- {pybgpkit-0.7.0 → pybgpkit-0.9.0}/pyproject.toml +2 -2
- pybgpkit-0.9.0/tests/test_asn_unit.py +87 -0
- pybgpkit-0.9.0/tests/test_broker_unit.py +237 -0
- pybgpkit-0.9.0/tests/test_community_unit.py +103 -0
- {pybgpkit-0.7.0 → pybgpkit-0.9.0}/tests/test_integration.py +23 -0
- pybgpkit-0.9.0/tests/test_roas_unit.py +90 -0
- pybgpkit-0.9.0/tests/test_utils_unit.py +65 -0
- pybgpkit-0.7.0/bgpkit/bgpkit_asn.py +0 -70
- pybgpkit-0.7.0/bgpkit/bgpkit_broker.py +0 -176
- pybgpkit-0.7.0/bgpkit/bgpkit_community.py +0 -63
- pybgpkit-0.7.0/bgpkit/bgpkit_roas.py +0 -64
- pybgpkit-0.7.0/pybgpkit.egg-info/requires.txt +0 -5
- {pybgpkit-0.7.0 → pybgpkit-0.9.0}/LICENSE +0 -0
- {pybgpkit-0.7.0 → pybgpkit-0.9.0}/README.md +0 -0
- {pybgpkit-0.7.0 → pybgpkit-0.9.0}/bgpkit/bgpkit_ip.py +0 -0
- {pybgpkit-0.7.0 → pybgpkit-0.9.0}/bgpkit/bgpkit_parser.py +0 -0
- {pybgpkit-0.7.0 → pybgpkit-0.9.0}/pybgpkit.egg-info/dependency_links.txt +0 -0
- {pybgpkit-0.7.0 → pybgpkit-0.9.0}/pybgpkit.egg-info/top_level.txt +0 -0
- {pybgpkit-0.7.0 → pybgpkit-0.9.0}/setup.cfg +0 -0
- {pybgpkit-0.7.0 → pybgpkit-0.9.0}/tests/test_smoke.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pybgpkit
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.9.0
|
|
4
4
|
Summary: BGPKIT tools Python bindings
|
|
5
5
|
Author-email: Mingwei Zhang <mingwei@bgpkit.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -13,7 +13,7 @@ Classifier: Programming Language :: Python :: 3.13
|
|
|
13
13
|
Requires-Python: >=3.9
|
|
14
14
|
Description-Content-Type: text/markdown
|
|
15
15
|
License-File: LICENSE
|
|
16
|
-
Requires-Dist: pybgpkit-parser>=0.
|
|
16
|
+
Requires-Dist: pybgpkit-parser>=0.18.0
|
|
17
17
|
Requires-Dist: requests
|
|
18
18
|
Provides-Extra: dev
|
|
19
19
|
Requires-Dist: pytest; extra == "dev"
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""Shared helpers for the BGPKIT API clients."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import fields
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
import requests
|
|
7
|
+
|
|
8
|
+
DEFAULT_TIMEOUT = 30.0
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class BGPKITApiError(RuntimeError):
|
|
12
|
+
"""Raised when an API request fails or returns an error response."""
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def from_item(cls, item: dict):
|
|
16
|
+
"""Build a dataclass from an API item, ignoring unknown fields."""
|
|
17
|
+
names = {field.name for field in fields(cls)}
|
|
18
|
+
return cls(**{key: value for key, value in item.items() if key in names})
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def request_json(
|
|
22
|
+
url: str,
|
|
23
|
+
params: Optional[dict] = None,
|
|
24
|
+
verify: bool = True,
|
|
25
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
26
|
+
) -> dict:
|
|
27
|
+
"""GET a JSON API endpoint, raising BGPKITApiError on failures."""
|
|
28
|
+
try:
|
|
29
|
+
response = requests.get(url, params=params or {}, verify=verify, timeout=timeout)
|
|
30
|
+
except requests.RequestException as error:
|
|
31
|
+
raise BGPKITApiError(f"GET {url} failed: {error}") from error
|
|
32
|
+
if response.status_code >= 400:
|
|
33
|
+
raise BGPKITApiError(
|
|
34
|
+
f"GET {url} failed with HTTP {response.status_code}: {response.text[:200]}"
|
|
35
|
+
)
|
|
36
|
+
try:
|
|
37
|
+
return response.json()
|
|
38
|
+
except ValueError as error:
|
|
39
|
+
raise BGPKITApiError(f"GET {url} returned a non-JSON response") from error
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import List, Optional
|
|
3
|
+
|
|
4
|
+
from ._utils import DEFAULT_TIMEOUT, from_item, request_json
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class Pagination:
|
|
9
|
+
page: int = 0
|
|
10
|
+
page_size: int = 0
|
|
11
|
+
total_pages: int = 0
|
|
12
|
+
total_count: int = 0
|
|
13
|
+
has_next: bool = False
|
|
14
|
+
has_prev: bool = False
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class AsnInfo:
|
|
19
|
+
asn: int
|
|
20
|
+
name: str
|
|
21
|
+
country: str = ""
|
|
22
|
+
country_name: str = ""
|
|
23
|
+
org_name: str = ""
|
|
24
|
+
org_id: str = ""
|
|
25
|
+
as2org: Optional[dict] = None
|
|
26
|
+
hegemony: Optional[dict] = None
|
|
27
|
+
peeringdb: Optional[dict] = None
|
|
28
|
+
population: Optional[dict] = None
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass
|
|
32
|
+
class AsnLookupResult:
|
|
33
|
+
data: List[AsnInfo]
|
|
34
|
+
count: int
|
|
35
|
+
page: int
|
|
36
|
+
page_size: int
|
|
37
|
+
updated_at: str = ""
|
|
38
|
+
pagination: Optional[Pagination] = None
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class AsnLookup:
|
|
42
|
+
"""BGPKIT ASN information lookup (v3/utils/asn)."""
|
|
43
|
+
|
|
44
|
+
def __init__(
|
|
45
|
+
self,
|
|
46
|
+
api_url: str = "https://api.bgpkit.com/v3/utils",
|
|
47
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
48
|
+
):
|
|
49
|
+
self.base_url = api_url.rstrip("/")
|
|
50
|
+
self.timeout = timeout
|
|
51
|
+
|
|
52
|
+
def query(
|
|
53
|
+
self,
|
|
54
|
+
asn: str = None,
|
|
55
|
+
country: str = None,
|
|
56
|
+
search: str = None,
|
|
57
|
+
page: int = 0,
|
|
58
|
+
page_size: int = 100,
|
|
59
|
+
) -> AsnLookupResult:
|
|
60
|
+
"""Look up ASN information.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
asn: AS number, or a comma-separated list of AS numbers.
|
|
64
|
+
country: Filter by ISO country code.
|
|
65
|
+
search: Free-text search over ASN names and organizations.
|
|
66
|
+
page: Page number to start from (0-indexed).
|
|
67
|
+
page_size: Results per page.
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
AsnLookupResult with the parsed ASN entries and pagination info.
|
|
71
|
+
"""
|
|
72
|
+
params = {}
|
|
73
|
+
if asn:
|
|
74
|
+
params["asn"] = asn
|
|
75
|
+
if country:
|
|
76
|
+
params["country"] = country
|
|
77
|
+
if search:
|
|
78
|
+
params["search"] = search
|
|
79
|
+
if page is not None:
|
|
80
|
+
params["page"] = page
|
|
81
|
+
if page_size:
|
|
82
|
+
params["page_size"] = min(page_size, 10000)
|
|
83
|
+
|
|
84
|
+
response = request_json(f"{self.base_url}/asn", params=params, timeout=self.timeout)
|
|
85
|
+
|
|
86
|
+
data = []
|
|
87
|
+
for item in response.get("data", []):
|
|
88
|
+
as2org = item.get("as2org") if isinstance(item.get("as2org"), dict) else {}
|
|
89
|
+
data.append(
|
|
90
|
+
AsnInfo(
|
|
91
|
+
asn=item.get("asn"),
|
|
92
|
+
name=item.get("name", ""),
|
|
93
|
+
country=item.get("country", ""),
|
|
94
|
+
country_name=item.get("country_name", ""),
|
|
95
|
+
org_name=as2org.get("org_name", ""),
|
|
96
|
+
org_id=as2org.get("org_id", ""),
|
|
97
|
+
as2org=as2org or None,
|
|
98
|
+
hegemony=item.get("hegemony"),
|
|
99
|
+
peeringdb=item.get("peeringdb"),
|
|
100
|
+
population=item.get("population"),
|
|
101
|
+
)
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
pagination_data = response.get("pagination")
|
|
105
|
+
pagination = (
|
|
106
|
+
from_item(Pagination, pagination_data) if isinstance(pagination_data, dict) else None
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
return AsnLookupResult(
|
|
110
|
+
data=data,
|
|
111
|
+
count=response.get("count", 0),
|
|
112
|
+
page=response.get("page", page),
|
|
113
|
+
page_size=response.get("page_size", page_size),
|
|
114
|
+
updated_at=response.get("updatedAt", ""),
|
|
115
|
+
pagination=pagination,
|
|
116
|
+
)
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import List, Optional
|
|
5
|
+
|
|
6
|
+
import urllib3
|
|
7
|
+
|
|
8
|
+
from ._utils import DEFAULT_TIMEOUT, BGPKITApiError, from_item, request_json
|
|
9
|
+
|
|
10
|
+
DEFAULT_API_URL = "https://api.bgpkit.com/v3/broker"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def check_type(value: any, ty: type) -> bool:
|
|
14
|
+
try:
|
|
15
|
+
ty(value)
|
|
16
|
+
return True
|
|
17
|
+
except (ValueError, TypeError):
|
|
18
|
+
raise ValueError("invalid option input")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class BrokerItem:
|
|
23
|
+
ts_start: str
|
|
24
|
+
ts_end: str
|
|
25
|
+
collector_id: str
|
|
26
|
+
data_type: str
|
|
27
|
+
url: str
|
|
28
|
+
rough_size: int
|
|
29
|
+
exact_size: int
|
|
30
|
+
delay: Optional[float] = None # seconds since the latest catalog update; /latest items only
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
@dataclass
|
|
34
|
+
class PeerItem:
|
|
35
|
+
date: str
|
|
36
|
+
collector: str
|
|
37
|
+
ip: str
|
|
38
|
+
asn: int
|
|
39
|
+
num_v4_pfxs: int
|
|
40
|
+
num_v6_pfxs: int
|
|
41
|
+
num_connected_asns: int
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def full_feed(self) -> bool:
|
|
45
|
+
"""True for full-table peers (>700k IPv4 or >100k IPv6 prefixes)."""
|
|
46
|
+
return self.num_v4_pfxs > 700_000 or self.num_v6_pfxs > 100_000
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass
|
|
50
|
+
class CollectorItem:
|
|
51
|
+
name: str
|
|
52
|
+
project: str
|
|
53
|
+
data_url: str
|
|
54
|
+
activated_on: str
|
|
55
|
+
deactivated_on: Optional[str]
|
|
56
|
+
country: str
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def active(self) -> bool:
|
|
60
|
+
return self.deactivated_on is None
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class Broker:
|
|
64
|
+
"""BGPKIT Broker v3 API wrapper.
|
|
65
|
+
|
|
66
|
+
Provides access to MRT data file search, peer information,
|
|
67
|
+
collector metadata, and latest file discovery.
|
|
68
|
+
|
|
69
|
+
The API base URL can be overridden per instance with ``api_url`` or for the
|
|
70
|
+
whole process with the ``BGPKIT_BROKER_URL`` environment variable.
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
def __init__(
|
|
74
|
+
self,
|
|
75
|
+
api_url: Optional[str] = None,
|
|
76
|
+
page_size: int = 100,
|
|
77
|
+
verify: bool = True,
|
|
78
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
79
|
+
):
|
|
80
|
+
if api_url is None:
|
|
81
|
+
api_url = os.environ.get("BGPKIT_BROKER_URL") or DEFAULT_API_URL
|
|
82
|
+
self.base_url = api_url.rstrip("/")
|
|
83
|
+
self.page_size = int(page_size)
|
|
84
|
+
self.verify = verify
|
|
85
|
+
self.timeout = timeout
|
|
86
|
+
if not verify:
|
|
87
|
+
urllib3.disable_warnings()
|
|
88
|
+
|
|
89
|
+
def _request(self, endpoint: str, params: Optional[dict] = None) -> dict:
|
|
90
|
+
"""Make a single request and return the parsed JSON body."""
|
|
91
|
+
return request_json(
|
|
92
|
+
f"{self.base_url}/{endpoint}",
|
|
93
|
+
params=params,
|
|
94
|
+
verify=self.verify,
|
|
95
|
+
timeout=self.timeout,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
def _paginate(self, endpoint: str, params: dict) -> dict:
|
|
99
|
+
"""Fetch every page of a paginated broker endpoint.
|
|
100
|
+
|
|
101
|
+
Stops when the reported total is reached, or when a page adds no new
|
|
102
|
+
items. The latter also terminates endpoints that ignore pagination
|
|
103
|
+
parameters instead of looping on the same full page forever.
|
|
104
|
+
"""
|
|
105
|
+
page = 1
|
|
106
|
+
all_data: List[dict] = []
|
|
107
|
+
seen = set()
|
|
108
|
+
result = {}
|
|
109
|
+
while True:
|
|
110
|
+
page_params = dict(params)
|
|
111
|
+
page_params["page"] = page
|
|
112
|
+
page_params["page_size"] = self.page_size
|
|
113
|
+
|
|
114
|
+
result = self._request(endpoint, page_params)
|
|
115
|
+
data = result.get("data", [])
|
|
116
|
+
new_items = []
|
|
117
|
+
for item in data:
|
|
118
|
+
key = json.dumps(item, sort_keys=True, default=str)
|
|
119
|
+
if key not in seen:
|
|
120
|
+
seen.add(key)
|
|
121
|
+
new_items.append(item)
|
|
122
|
+
all_data.extend(new_items)
|
|
123
|
+
|
|
124
|
+
total = result.get("total")
|
|
125
|
+
if not data or not new_items:
|
|
126
|
+
break
|
|
127
|
+
if total is not None and len(all_data) >= int(total):
|
|
128
|
+
break
|
|
129
|
+
page += 1
|
|
130
|
+
|
|
131
|
+
result["data"] = all_data
|
|
132
|
+
return result
|
|
133
|
+
|
|
134
|
+
def query(
|
|
135
|
+
self,
|
|
136
|
+
ts_start: str = None,
|
|
137
|
+
ts_end: str = None,
|
|
138
|
+
collector_id: str = None,
|
|
139
|
+
project: str = None,
|
|
140
|
+
data_type: str = None,
|
|
141
|
+
) -> List[BrokerItem]:
|
|
142
|
+
"""Search for MRT data files matching the given criteria."""
|
|
143
|
+
params = {}
|
|
144
|
+
if ts_start:
|
|
145
|
+
params["ts_start"] = ts_start
|
|
146
|
+
if ts_end:
|
|
147
|
+
params["ts_end"] = ts_end
|
|
148
|
+
if collector_id:
|
|
149
|
+
params["collector_id"] = collector_id
|
|
150
|
+
if project:
|
|
151
|
+
check_type(project, str)
|
|
152
|
+
params["project"] = project
|
|
153
|
+
if data_type:
|
|
154
|
+
check_type(data_type, str)
|
|
155
|
+
params["data_type"] = data_type
|
|
156
|
+
|
|
157
|
+
result = self._paginate("search", params)
|
|
158
|
+
return [from_item(BrokerItem, item) for item in result.get("data", [])]
|
|
159
|
+
|
|
160
|
+
def latest(self) -> List[BrokerItem]:
|
|
161
|
+
"""Get the latest MRT data file for every collector and data type."""
|
|
162
|
+
result = self._request("latest")
|
|
163
|
+
return [from_item(BrokerItem, item) for item in result.get("data", [])]
|
|
164
|
+
|
|
165
|
+
def peers(
|
|
166
|
+
self,
|
|
167
|
+
full_feed: bool = None,
|
|
168
|
+
ip: str = None,
|
|
169
|
+
asn: int = None,
|
|
170
|
+
collector: str = None,
|
|
171
|
+
) -> List[PeerItem]:
|
|
172
|
+
"""Query BGP peer information."""
|
|
173
|
+
params = {}
|
|
174
|
+
if full_feed is not None:
|
|
175
|
+
params["full_feed"] = str(full_feed).lower()
|
|
176
|
+
if ip:
|
|
177
|
+
params["ip"] = ip
|
|
178
|
+
if asn:
|
|
179
|
+
params["asn"] = asn
|
|
180
|
+
if collector:
|
|
181
|
+
params["collector"] = collector
|
|
182
|
+
|
|
183
|
+
result = self._request("peers", params)
|
|
184
|
+
return [from_item(PeerItem, item) for item in result.get("data", [])]
|
|
185
|
+
|
|
186
|
+
def collectors(
|
|
187
|
+
self,
|
|
188
|
+
project: str = None,
|
|
189
|
+
country: str = None,
|
|
190
|
+
active: bool = None,
|
|
191
|
+
) -> List[CollectorItem]:
|
|
192
|
+
"""Query MRT collector information."""
|
|
193
|
+
params = {}
|
|
194
|
+
if project:
|
|
195
|
+
params["project"] = project
|
|
196
|
+
if country:
|
|
197
|
+
params["country"] = country
|
|
198
|
+
if active is not None:
|
|
199
|
+
params["active"] = str(active).lower()
|
|
200
|
+
|
|
201
|
+
result = self._request("collectors", params)
|
|
202
|
+
return [from_item(CollectorItem, item) for item in result.get("data", [])]
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import List, Optional
|
|
3
|
+
|
|
4
|
+
from ._utils import DEFAULT_TIMEOUT, from_item, request_json
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass
|
|
8
|
+
class CommunityEntry:
|
|
9
|
+
asn: int
|
|
10
|
+
value: str
|
|
11
|
+
description: str
|
|
12
|
+
as_name: str = ""
|
|
13
|
+
country: str = ""
|
|
14
|
+
source: str = ""
|
|
15
|
+
range: Optional[str] = None
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class CommunitySource:
|
|
20
|
+
id: str
|
|
21
|
+
name: str
|
|
22
|
+
url: str = ""
|
|
23
|
+
last_updated: Optional[str] = None
|
|
24
|
+
stats: Optional[dict] = None
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class CommunityLookup:
|
|
28
|
+
"""BGPKIT community lookup (v3/communities)."""
|
|
29
|
+
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
api_url: str = "https://api.bgpkit.com/v3/communities",
|
|
33
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
34
|
+
):
|
|
35
|
+
self.base_url = api_url.rstrip("/")
|
|
36
|
+
self.timeout = timeout
|
|
37
|
+
|
|
38
|
+
def query(
|
|
39
|
+
self,
|
|
40
|
+
asn: str = None,
|
|
41
|
+
value: str = None,
|
|
42
|
+
description: str = None,
|
|
43
|
+
as_name: str = None,
|
|
44
|
+
country: str = None,
|
|
45
|
+
page: int = 0,
|
|
46
|
+
page_size: int = 10,
|
|
47
|
+
) -> List[CommunityEntry]:
|
|
48
|
+
"""Query BGP communities.
|
|
49
|
+
|
|
50
|
+
The API returns one entry per ASN with a nested list of communities;
|
|
51
|
+
this returns one CommunityEntry per community.
|
|
52
|
+
"""
|
|
53
|
+
params = {}
|
|
54
|
+
if asn:
|
|
55
|
+
params["asn"] = asn
|
|
56
|
+
if value:
|
|
57
|
+
params["value"] = value
|
|
58
|
+
if description:
|
|
59
|
+
params["description"] = description
|
|
60
|
+
if as_name:
|
|
61
|
+
params["as_name"] = as_name
|
|
62
|
+
if country:
|
|
63
|
+
params["country"] = country
|
|
64
|
+
params["page"] = str(page)
|
|
65
|
+
params["page_size"] = str(min(page_size, 1000))
|
|
66
|
+
|
|
67
|
+
response = request_json(self.base_url, params=params, timeout=self.timeout)
|
|
68
|
+
|
|
69
|
+
entries = []
|
|
70
|
+
for item in response.get("data", []):
|
|
71
|
+
for community in item.get("communities", []) or []:
|
|
72
|
+
entries.append(
|
|
73
|
+
CommunityEntry(
|
|
74
|
+
asn=item.get("asn"),
|
|
75
|
+
value=community.get("community", ""),
|
|
76
|
+
description=community.get("description") or "",
|
|
77
|
+
as_name=item.get("name", ""),
|
|
78
|
+
country=item.get("country", ""),
|
|
79
|
+
source=community.get("source", ""),
|
|
80
|
+
range=community.get("range"),
|
|
81
|
+
)
|
|
82
|
+
)
|
|
83
|
+
return entries
|
|
84
|
+
|
|
85
|
+
def sources(self) -> List[CommunitySource]:
|
|
86
|
+
"""List the community data sources."""
|
|
87
|
+
response = request_json(f"{self.base_url}/sources", timeout=self.timeout)
|
|
88
|
+
if isinstance(response, list):
|
|
89
|
+
return [from_item(CommunitySource, item) for item in response]
|
|
90
|
+
return [from_item(CommunitySource, item) for item in response.get("sources", [])]
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from typing import List, Optional
|
|
3
|
+
|
|
4
|
+
from ._utils import DEFAULT_TIMEOUT, from_item, request_json
|
|
5
|
+
|
|
6
|
+
DEFAULT_API_URL = "https://api.bgpkit.com/v3/roas"
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class RoasItem:
|
|
11
|
+
prefix: str
|
|
12
|
+
asn: int
|
|
13
|
+
max_len: Optional[int] = None
|
|
14
|
+
tal: Optional[str] = None
|
|
15
|
+
current: bool = False
|
|
16
|
+
date_ranges: Optional[List[List[str]]] = None
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Roas:
|
|
20
|
+
"""BGPKIT ROAS lookup (v3/roas).
|
|
21
|
+
|
|
22
|
+
Queries the ROAS (Route Origin Authorization) database for historical and
|
|
23
|
+
current RPKI data. The base URL can be overridden per instance with
|
|
24
|
+
``api_url``.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def __init__(
|
|
28
|
+
self,
|
|
29
|
+
api_url: str = DEFAULT_API_URL,
|
|
30
|
+
page_size: int = 100,
|
|
31
|
+
timeout: float = DEFAULT_TIMEOUT,
|
|
32
|
+
):
|
|
33
|
+
self.base_url = api_url.rstrip("/")
|
|
34
|
+
self.page_size = int(page_size)
|
|
35
|
+
self.timeout = timeout
|
|
36
|
+
|
|
37
|
+
def query(
|
|
38
|
+
self,
|
|
39
|
+
asn: int = None,
|
|
40
|
+
prefix: str = None,
|
|
41
|
+
date: str = None,
|
|
42
|
+
current: bool = None,
|
|
43
|
+
page: int = 0,
|
|
44
|
+
page_size: int = None,
|
|
45
|
+
) -> List[RoasItem]:
|
|
46
|
+
"""Query the ROAS database, fetching every page from ``page`` onward.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
asn: AS number to filter by.
|
|
50
|
+
prefix: IP prefix to filter by.
|
|
51
|
+
date: Date string (YYYY-MM-DD) for historical lookup.
|
|
52
|
+
current: If True, return only currently valid ROAs.
|
|
53
|
+
page: Page number to start from (0-indexed).
|
|
54
|
+
page_size: Results per page (defaults to the instance page size).
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
List of RoasItem matching the query.
|
|
58
|
+
"""
|
|
59
|
+
params = {}
|
|
60
|
+
if asn is not None:
|
|
61
|
+
params["asn"] = asn
|
|
62
|
+
if prefix:
|
|
63
|
+
params["prefix"] = prefix
|
|
64
|
+
if date:
|
|
65
|
+
params["date"] = date
|
|
66
|
+
if current is not None:
|
|
67
|
+
params["current"] = str(current).lower()
|
|
68
|
+
|
|
69
|
+
size = int(page_size) if page_size is not None else self.page_size
|
|
70
|
+
items: List[RoasItem] = []
|
|
71
|
+
while True:
|
|
72
|
+
page_params = dict(params)
|
|
73
|
+
page_params["page"] = page
|
|
74
|
+
page_params["page_size"] = size
|
|
75
|
+
response = request_json(f"{self.base_url}/search", params=page_params, timeout=self.timeout)
|
|
76
|
+
|
|
77
|
+
data = response.get("data", [])
|
|
78
|
+
items.extend(from_item(RoasItem, item) for item in data)
|
|
79
|
+
|
|
80
|
+
total = response.get("total")
|
|
81
|
+
if not data or (total is not None and len(items) >= int(total)):
|
|
82
|
+
break
|
|
83
|
+
page += 1
|
|
84
|
+
|
|
85
|
+
return items
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pybgpkit
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.9.0
|
|
4
4
|
Summary: BGPKIT tools Python bindings
|
|
5
5
|
Author-email: Mingwei Zhang <mingwei@bgpkit.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -13,7 +13,7 @@ Classifier: Programming Language :: Python :: 3.13
|
|
|
13
13
|
Requires-Python: >=3.9
|
|
14
14
|
Description-Content-Type: text/markdown
|
|
15
15
|
License-File: LICENSE
|
|
16
|
-
Requires-Dist: pybgpkit-parser>=0.
|
|
16
|
+
Requires-Dist: pybgpkit-parser>=0.18.0
|
|
17
17
|
Requires-Dist: requests
|
|
18
18
|
Provides-Extra: dev
|
|
19
19
|
Requires-Dist: pytest; extra == "dev"
|
|
@@ -2,6 +2,7 @@ LICENSE
|
|
|
2
2
|
README.md
|
|
3
3
|
pyproject.toml
|
|
4
4
|
bgpkit/__init__.py
|
|
5
|
+
bgpkit/_utils.py
|
|
5
6
|
bgpkit/bgpkit_asn.py
|
|
6
7
|
bgpkit/bgpkit_broker.py
|
|
7
8
|
bgpkit/bgpkit_community.py
|
|
@@ -13,5 +14,10 @@ pybgpkit.egg-info/SOURCES.txt
|
|
|
13
14
|
pybgpkit.egg-info/dependency_links.txt
|
|
14
15
|
pybgpkit.egg-info/requires.txt
|
|
15
16
|
pybgpkit.egg-info/top_level.txt
|
|
17
|
+
tests/test_asn_unit.py
|
|
18
|
+
tests/test_broker_unit.py
|
|
19
|
+
tests/test_community_unit.py
|
|
16
20
|
tests/test_integration.py
|
|
17
|
-
tests/
|
|
21
|
+
tests/test_roas_unit.py
|
|
22
|
+
tests/test_smoke.py
|
|
23
|
+
tests/test_utils_unit.py
|
|
@@ -5,7 +5,7 @@ build-backend = "setuptools.build_meta"
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "pybgpkit"
|
|
7
7
|
dynamic = []
|
|
8
|
-
version = "0.
|
|
8
|
+
version = "0.9.0"
|
|
9
9
|
description = "BGPKIT tools Python bindings"
|
|
10
10
|
readme = "README.md"
|
|
11
11
|
requires-python = ">=3.9"
|
|
@@ -21,7 +21,7 @@ classifiers = [
|
|
|
21
21
|
"Programming Language :: Python :: 3.13",
|
|
22
22
|
]
|
|
23
23
|
dependencies = [
|
|
24
|
-
"pybgpkit-parser>=0.
|
|
24
|
+
"pybgpkit-parser>=0.18.0",
|
|
25
25
|
"requests",
|
|
26
26
|
]
|
|
27
27
|
|