fastmile-parser 0.1.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.
- fastmile_parser-0.1.0/LICENSE +21 -0
- fastmile_parser-0.1.0/PKG-INFO +33 -0
- fastmile_parser-0.1.0/README.md +19 -0
- fastmile_parser-0.1.0/pyproject.toml +26 -0
- fastmile_parser-0.1.0/setup.cfg +4 -0
- fastmile_parser-0.1.0/src/fastmile_parser/__init__.py +1 -0
- fastmile_parser-0.1.0/src/fastmile_parser/models.py +75 -0
- fastmile_parser-0.1.0/src/fastmile_parser/router_client.py +18 -0
- fastmile_parser-0.1.0/src/fastmile_parser/scraper.py +211 -0
- fastmile_parser-0.1.0/src/fastmile_parser.egg-info/PKG-INFO +33 -0
- fastmile_parser-0.1.0/src/fastmile_parser.egg-info/SOURCES.txt +14 -0
- fastmile_parser-0.1.0/src/fastmile_parser.egg-info/dependency_links.txt +1 -0
- fastmile_parser-0.1.0/src/fastmile_parser.egg-info/requires.txt +5 -0
- fastmile_parser-0.1.0/src/fastmile_parser.egg-info/top_level.txt +1 -0
- fastmile_parser-0.1.0/tests/test_router_client.py +26 -0
- fastmile_parser-0.1.0/tests/test_scraper.py +101 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 AdrianKlm
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fastmile-parser
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Shared Nokia FastMile router snapshot parser
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Requires-Python: >=3.11
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Requires-Dist: requests>=2.32
|
|
10
|
+
Requires-Dist: beautifulsoup4>=4.12
|
|
11
|
+
Provides-Extra: dev
|
|
12
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
13
|
+
Dynamic: license-file
|
|
14
|
+
|
|
15
|
+
# fastmile-parser
|
|
16
|
+
|
|
17
|
+
Shared Nokia FastMile parser package.
|
|
18
|
+
|
|
19
|
+
## What it does
|
|
20
|
+
|
|
21
|
+
- fetches `https://<router>/status.php`
|
|
22
|
+
- parses the status page into typed snapshot models
|
|
23
|
+
- preserves the exact field set used by `fastmile-api`
|
|
24
|
+
|
|
25
|
+
## Usage
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
from fastmile_parser.router_client import RouterClient
|
|
29
|
+
from fastmile_parser.scraper import parse_snapshot
|
|
30
|
+
|
|
31
|
+
html = RouterClient("192.168.0.1").fetch_status_html()
|
|
32
|
+
snapshot = parse_snapshot(html)
|
|
33
|
+
```
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# fastmile-parser
|
|
2
|
+
|
|
3
|
+
Shared Nokia FastMile parser package.
|
|
4
|
+
|
|
5
|
+
## What it does
|
|
6
|
+
|
|
7
|
+
- fetches `https://<router>/status.php`
|
|
8
|
+
- parses the status page into typed snapshot models
|
|
9
|
+
- preserves the exact field set used by `fastmile-api`
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```python
|
|
14
|
+
from fastmile_parser.router_client import RouterClient
|
|
15
|
+
from fastmile_parser.scraper import parse_snapshot
|
|
16
|
+
|
|
17
|
+
html = RouterClient("192.168.0.1").fetch_status_html()
|
|
18
|
+
snapshot = parse_snapshot(html)
|
|
19
|
+
```
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "fastmile-parser"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Shared Nokia FastMile router snapshot parser"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.11"
|
|
12
|
+
dependencies = [
|
|
13
|
+
"requests>=2.32",
|
|
14
|
+
"beautifulsoup4>=4.12",
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
[project.optional-dependencies]
|
|
18
|
+
dev = [
|
|
19
|
+
"pytest>=8.0",
|
|
20
|
+
]
|
|
21
|
+
|
|
22
|
+
[tool.setuptools]
|
|
23
|
+
package-dir = {"" = "src"}
|
|
24
|
+
|
|
25
|
+
[tool.setuptools.packages.find]
|
|
26
|
+
where = ["src"]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""FastMile parser package."""
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@dataclass(slots=True)
|
|
8
|
+
class DeviceInfo:
|
|
9
|
+
model: str
|
|
10
|
+
software_version: str
|
|
11
|
+
serial_number: str
|
|
12
|
+
imei: str
|
|
13
|
+
imsi: str
|
|
14
|
+
mac: str
|
|
15
|
+
lock_status: str
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(slots=True)
|
|
19
|
+
class TrafficValue:
|
|
20
|
+
val: float
|
|
21
|
+
unit: str
|
|
22
|
+
val_gb: float
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass(slots=True)
|
|
26
|
+
class InterfaceData:
|
|
27
|
+
download: TrafficValue
|
|
28
|
+
upload: TrafficValue
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass(slots=True)
|
|
32
|
+
class ApnInfo:
|
|
33
|
+
name: str
|
|
34
|
+
ipv4: Optional[str]
|
|
35
|
+
ipv6: Optional[str]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass(slots=True)
|
|
39
|
+
class CaInfo:
|
|
40
|
+
enb: int
|
|
41
|
+
cid: int
|
|
42
|
+
dl_bands: list[int]
|
|
43
|
+
ul_bands: list[int]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass(slots=True)
|
|
47
|
+
class CellSignal:
|
|
48
|
+
pci: int
|
|
49
|
+
earfcn: int
|
|
50
|
+
cell_type: Optional[str]
|
|
51
|
+
rsrp: int
|
|
52
|
+
rsrq: int
|
|
53
|
+
rssi: int
|
|
54
|
+
sinr: int
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@dataclass(slots=True)
|
|
58
|
+
class LteInfo:
|
|
59
|
+
ca: CaInfo
|
|
60
|
+
active: list[CellSignal]
|
|
61
|
+
available: list[CellSignal]
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass(slots=True)
|
|
65
|
+
class SnapshotData:
|
|
66
|
+
eth: InterfaceData
|
|
67
|
+
lte: InterfaceData
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@dataclass(slots=True)
|
|
71
|
+
class Snapshot:
|
|
72
|
+
device: DeviceInfo
|
|
73
|
+
apns: list[ApnInfo]
|
|
74
|
+
data: SnapshotData
|
|
75
|
+
lte: LteInfo
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import requests
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class RouterClient:
|
|
7
|
+
def __init__(self, host: str, timeout: int = 30) -> None:
|
|
8
|
+
self.host = host
|
|
9
|
+
self.timeout = timeout
|
|
10
|
+
|
|
11
|
+
def fetch_status_html(self) -> str:
|
|
12
|
+
response = requests.get(
|
|
13
|
+
f"https://{self.host}/status.php",
|
|
14
|
+
timeout=self.timeout,
|
|
15
|
+
verify=False,
|
|
16
|
+
)
|
|
17
|
+
response.raise_for_status()
|
|
18
|
+
return response.text
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from typing import Optional
|
|
5
|
+
|
|
6
|
+
from bs4 import BeautifulSoup, Comment, Tag
|
|
7
|
+
|
|
8
|
+
from fastmile_parser.models import (
|
|
9
|
+
ApnInfo,
|
|
10
|
+
CaInfo,
|
|
11
|
+
CellSignal,
|
|
12
|
+
DeviceInfo,
|
|
13
|
+
InterfaceData,
|
|
14
|
+
LteInfo,
|
|
15
|
+
Snapshot,
|
|
16
|
+
SnapshotData,
|
|
17
|
+
TrafficValue,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _text(tag: Tag) -> str:
|
|
22
|
+
return tag.get_text(strip=True)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _first_text(soup: BeautifulSoup, element_id: str) -> str:
|
|
26
|
+
tag = soup.find(id=element_id)
|
|
27
|
+
if tag is None:
|
|
28
|
+
raise ValueError(f"missing element: {element_id}")
|
|
29
|
+
return _text(tag)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _parse_int(value: str) -> int:
|
|
33
|
+
match = re.search(r"-?\d+", value)
|
|
34
|
+
if not match:
|
|
35
|
+
raise ValueError(f"expected integer in: {value!r}")
|
|
36
|
+
return int(match.group(0))
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _parse_ip(value: str) -> Optional[str]:
|
|
40
|
+
value = value.strip()
|
|
41
|
+
return None if value in {"", "::"} else value
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _parse_traffic(value: str) -> TrafficValue:
|
|
45
|
+
cleaned = value.strip().upper()
|
|
46
|
+
match = re.match(r"([0-9.]+)\s*([A-Z]+)", cleaned)
|
|
47
|
+
if not match:
|
|
48
|
+
raise ValueError(f"invalid traffic value: {value!r}")
|
|
49
|
+
amount = float(match.group(1))
|
|
50
|
+
unit = match.group(2)
|
|
51
|
+
if unit == "MB":
|
|
52
|
+
val_gb = amount / 1000
|
|
53
|
+
elif unit == "GB":
|
|
54
|
+
val_gb = amount
|
|
55
|
+
elif unit == "KB":
|
|
56
|
+
val_gb = amount / 1_000_000
|
|
57
|
+
else:
|
|
58
|
+
val_gb = amount
|
|
59
|
+
return TrafficValue(val=amount, unit=unit, val_gb=val_gb)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _find_comment_section(soup: BeautifulSoup, comment_text: str) -> Optional[Tag]:
|
|
63
|
+
comment = soup.find(string=lambda text: isinstance(text, Comment) and text.strip() == comment_text)
|
|
64
|
+
if comment is None:
|
|
65
|
+
return None
|
|
66
|
+
sibling = comment.find_next_sibling()
|
|
67
|
+
while sibling is not None and not isinstance(sibling, Tag):
|
|
68
|
+
sibling = sibling.find_next_sibling()
|
|
69
|
+
return sibling
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _parse_apns(soup: BeautifulSoup) -> list[ApnInfo]:
|
|
73
|
+
section = _find_comment_section(soup, "APNs card")
|
|
74
|
+
if section is None:
|
|
75
|
+
return []
|
|
76
|
+
values = section.find_all("div", id=None)
|
|
77
|
+
if len(values) < 3:
|
|
78
|
+
return []
|
|
79
|
+
result: list[ApnInfo] = []
|
|
80
|
+
for index in range(2, len(values), 3):
|
|
81
|
+
if index + 2 >= len(values):
|
|
82
|
+
break
|
|
83
|
+
result.append(
|
|
84
|
+
ApnInfo(
|
|
85
|
+
name=_text(values[index]),
|
|
86
|
+
ipv4=_parse_ip(_text(values[index + 1])),
|
|
87
|
+
ipv6=_parse_ip(_text(values[index + 2])),
|
|
88
|
+
)
|
|
89
|
+
)
|
|
90
|
+
return result
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _parse_cell_group(section: Tag, cell_type: Optional[str]) -> list[CellSignal]:
|
|
94
|
+
values = section.find_all(class_="name-of-value-in-card-bold")
|
|
95
|
+
result: list[CellSignal] = []
|
|
96
|
+
for index in range(0, len(values), 7):
|
|
97
|
+
chunk = values[index : index + 7]
|
|
98
|
+
if len(chunk) < 7:
|
|
99
|
+
break
|
|
100
|
+
result.append(
|
|
101
|
+
CellSignal(
|
|
102
|
+
pci=_parse_int(_text(chunk[0])),
|
|
103
|
+
earfcn=_parse_int(_text(chunk[1])),
|
|
104
|
+
cell_type=_text(chunk[2]) if cell_type is not None else None,
|
|
105
|
+
rsrp=_parse_int(_text(chunk[3])),
|
|
106
|
+
rsrq=_parse_int(_text(chunk[4])),
|
|
107
|
+
rssi=_parse_int(_text(chunk[5])),
|
|
108
|
+
sinr=_parse_int(_text(chunk[6])),
|
|
109
|
+
)
|
|
110
|
+
)
|
|
111
|
+
return result
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _parse_available_cells(soup: BeautifulSoup) -> list[CellSignal]:
|
|
115
|
+
result: list[CellSignal] = []
|
|
116
|
+
index = 0
|
|
117
|
+
while True:
|
|
118
|
+
cell_id = soup.find(id=f"available-cell-id-{index}")
|
|
119
|
+
if cell_id is None:
|
|
120
|
+
break
|
|
121
|
+
result.append(
|
|
122
|
+
CellSignal(
|
|
123
|
+
pci=_parse_int(_text(cell_id)),
|
|
124
|
+
earfcn=_parse_int(_first_text(soup, f"available-earfcn-{index}")),
|
|
125
|
+
cell_type=None,
|
|
126
|
+
rsrp=_parse_int(_first_text(soup, f"rsrp-{index}")),
|
|
127
|
+
rsrq=_parse_int(_first_text(soup, f"rsrq-{index}")),
|
|
128
|
+
rssi=_parse_int(_first_text(soup, f"rssi-{index}")),
|
|
129
|
+
sinr=_parse_int(_first_text(soup, f"sinr-{index}")),
|
|
130
|
+
)
|
|
131
|
+
)
|
|
132
|
+
index += 1
|
|
133
|
+
return result
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _parse_ca(soup: BeautifulSoup) -> CaInfo:
|
|
137
|
+
attached = soup.find(id="attached-cell-val")
|
|
138
|
+
if attached is None:
|
|
139
|
+
raise ValueError("missing attached-cell-val")
|
|
140
|
+
spans = attached.find_all("span")
|
|
141
|
+
if len(spans) < 3:
|
|
142
|
+
raise ValueError("missing carrier aggregation values")
|
|
143
|
+
enb = _parse_int(_text(spans[0]))
|
|
144
|
+
cid = _parse_int(_text(spans[1]))
|
|
145
|
+
primary_band = _parse_int(_text(spans[2]))
|
|
146
|
+
|
|
147
|
+
dl_text = _first_text(soup, "bandDL-val")
|
|
148
|
+
ul_text = _first_text(soup, "bandUL-val")
|
|
149
|
+
dl_bands = [primary_band]
|
|
150
|
+
ul_bands = [primary_band]
|
|
151
|
+
dl_match = re.findall(r"B(\d+)", dl_text)
|
|
152
|
+
ul_match = re.findall(r"B(\d+)", ul_text)
|
|
153
|
+
dl_bands.extend(int(value) for value in dl_match)
|
|
154
|
+
ul_bands.extend(int(value) for value in ul_match)
|
|
155
|
+
return CaInfo(enb=enb, cid=cid, dl_bands=dl_bands, ul_bands=ul_bands)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _parse_interface_data(soup: BeautifulSoup, name: str) -> InterfaceData:
|
|
159
|
+
section = soup.find(class_=name)
|
|
160
|
+
if section is None:
|
|
161
|
+
raise ValueError(f"missing traffic section: {name}")
|
|
162
|
+
bytes_values = section.find_all(class_="bytes")
|
|
163
|
+
if len(bytes_values) < 2:
|
|
164
|
+
raise ValueError(f"missing traffic values for: {name}")
|
|
165
|
+
return InterfaceData(
|
|
166
|
+
download=_parse_traffic(_text(bytes_values[0])),
|
|
167
|
+
upload=_parse_traffic(_text(bytes_values[1])),
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def parse_snapshot(html: str) -> Snapshot:
|
|
172
|
+
soup = BeautifulSoup(html, "html.parser")
|
|
173
|
+
|
|
174
|
+
device = DeviceInfo(
|
|
175
|
+
model=_first_text(soup, "model-value"),
|
|
176
|
+
software_version=_first_text(soup, "software-version-val"),
|
|
177
|
+
serial_number=_first_text(soup, "sn-value"),
|
|
178
|
+
imei=_first_text(soup, "imei-value"),
|
|
179
|
+
imsi=_first_text(soup, "imsi-name-value"),
|
|
180
|
+
mac=_first_text(soup, "eth-mac-value"),
|
|
181
|
+
lock_status=_first_text(soup, "lockStatus-name-value"),
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
data = SnapshotData(
|
|
185
|
+
eth=_parse_interface_data(soup, "Ethernet"),
|
|
186
|
+
lte=_parse_interface_data(soup, "LTE"),
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
primary_section = _find_comment_section(soup, "Primary Cell information card")
|
|
190
|
+
secondary_section = _find_comment_section(soup, "Secondary Cell information card")
|
|
191
|
+
available_section = _find_comment_section(soup, "Available cells grid (*x6)")
|
|
192
|
+
|
|
193
|
+
active: list[CellSignal] = []
|
|
194
|
+
if primary_section is not None:
|
|
195
|
+
active.extend(_parse_cell_group(primary_section, cell_type="primary"))
|
|
196
|
+
if secondary_section is not None:
|
|
197
|
+
active.extend(_parse_cell_group(secondary_section, cell_type="secondary"))
|
|
198
|
+
|
|
199
|
+
available: list[CellSignal] = []
|
|
200
|
+
if available_section is not None:
|
|
201
|
+
available = _parse_available_cells(soup)
|
|
202
|
+
|
|
203
|
+
lte = LteInfo(
|
|
204
|
+
ca=_parse_ca(soup),
|
|
205
|
+
active=active,
|
|
206
|
+
available=available,
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
apns = _parse_apns(soup)
|
|
210
|
+
|
|
211
|
+
return Snapshot(device=device, apns=apns, data=data, lte=lte)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fastmile-parser
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Shared Nokia FastMile router snapshot parser
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Requires-Python: >=3.11
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Requires-Dist: requests>=2.32
|
|
10
|
+
Requires-Dist: beautifulsoup4>=4.12
|
|
11
|
+
Provides-Extra: dev
|
|
12
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
13
|
+
Dynamic: license-file
|
|
14
|
+
|
|
15
|
+
# fastmile-parser
|
|
16
|
+
|
|
17
|
+
Shared Nokia FastMile parser package.
|
|
18
|
+
|
|
19
|
+
## What it does
|
|
20
|
+
|
|
21
|
+
- fetches `https://<router>/status.php`
|
|
22
|
+
- parses the status page into typed snapshot models
|
|
23
|
+
- preserves the exact field set used by `fastmile-api`
|
|
24
|
+
|
|
25
|
+
## Usage
|
|
26
|
+
|
|
27
|
+
```python
|
|
28
|
+
from fastmile_parser.router_client import RouterClient
|
|
29
|
+
from fastmile_parser.scraper import parse_snapshot
|
|
30
|
+
|
|
31
|
+
html = RouterClient("192.168.0.1").fetch_status_html()
|
|
32
|
+
snapshot = parse_snapshot(html)
|
|
33
|
+
```
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/fastmile_parser/__init__.py
|
|
5
|
+
src/fastmile_parser/models.py
|
|
6
|
+
src/fastmile_parser/router_client.py
|
|
7
|
+
src/fastmile_parser/scraper.py
|
|
8
|
+
src/fastmile_parser.egg-info/PKG-INFO
|
|
9
|
+
src/fastmile_parser.egg-info/SOURCES.txt
|
|
10
|
+
src/fastmile_parser.egg-info/dependency_links.txt
|
|
11
|
+
src/fastmile_parser.egg-info/requires.txt
|
|
12
|
+
src/fastmile_parser.egg-info/top_level.txt
|
|
13
|
+
tests/test_router_client.py
|
|
14
|
+
tests/test_scraper.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
fastmile_parser
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
|
|
3
|
+
from fastmile_parser.router_client import RouterClient
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def test_fetch_status_html_uses_https_and_verify_false(monkeypatch):
|
|
7
|
+
calls = {}
|
|
8
|
+
|
|
9
|
+
class Response:
|
|
10
|
+
status_code = 200
|
|
11
|
+
text = "<html>ok</html>"
|
|
12
|
+
|
|
13
|
+
def raise_for_status(self):
|
|
14
|
+
return None
|
|
15
|
+
|
|
16
|
+
def fake_get(url, timeout, verify):
|
|
17
|
+
calls["url"] = url
|
|
18
|
+
calls["timeout"] = timeout
|
|
19
|
+
calls["verify"] = verify
|
|
20
|
+
return Response()
|
|
21
|
+
|
|
22
|
+
monkeypatch.setattr(requests, "get", fake_get)
|
|
23
|
+
|
|
24
|
+
client = RouterClient("192.168.0.1", timeout=30)
|
|
25
|
+
assert client.fetch_status_html() == "<html>ok</html>"
|
|
26
|
+
assert calls == {"url": "https://192.168.0.1/status.php", "timeout": 30, "verify": False}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
from fastmile_parser.scraper import parse_snapshot
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
HTML = """
|
|
5
|
+
<html>
|
|
6
|
+
<div id="model-value">ODU - Multiband - 4G05-B</div>
|
|
7
|
+
<div id="software-version-val">FASTMILE2_D020110B83T0101M01E0153S</div>
|
|
8
|
+
<div id="sn-value">FSH22010081D</div>
|
|
9
|
+
<div id="imei-value">356088101152952</div>
|
|
10
|
+
<div id="imsi-name-value">260032773115333</div>
|
|
11
|
+
<div id="eth-mac-value">DC:8D:8A:97:47:C2</div>
|
|
12
|
+
<div id="lockStatus-name-value">Normal</div>
|
|
13
|
+
|
|
14
|
+
<div id="attached-cell-val">eNBID: <span>291067</span> Cell ID: <span>53</span> Band: <span>3</span></div>
|
|
15
|
+
<div id="bandDL-val">B1</div>
|
|
16
|
+
<div id="bandUL-val">CA Not Available</div>
|
|
17
|
+
|
|
18
|
+
<!-- Primary Cell information card -->
|
|
19
|
+
<div class="card-sevencol-grid">
|
|
20
|
+
<div class="name-of-value-in-card-bold">131</div>
|
|
21
|
+
<div class="name-of-value-in-card-bold">1725</div>
|
|
22
|
+
<div class="name-of-value-in-card-bold">CellPrimary</div>
|
|
23
|
+
<div class="name-of-value-in-card-bold">-106</div>
|
|
24
|
+
<div class="name-of-value-in-card-bold">-13</div>
|
|
25
|
+
<div class="name-of-value-in-card-bold">-74</div>
|
|
26
|
+
<div class="name-of-value-in-card-bold">6</div>
|
|
27
|
+
</div>
|
|
28
|
+
|
|
29
|
+
<!-- Secondary Cell information card -->
|
|
30
|
+
<div class="card-sevencol-grid">
|
|
31
|
+
<div class="name-of-value-in-card-bold">311</div>
|
|
32
|
+
<div class="name-of-value-in-card-bold">75</div>
|
|
33
|
+
<div class="name-of-value-in-card-bold">CellSecondary</div>
|
|
34
|
+
<div class="name-of-value-in-card-bold">-110</div>
|
|
35
|
+
<div class="name-of-value-in-card-bold">-14</div>
|
|
36
|
+
<div class="name-of-value-in-card-bold">-77</div>
|
|
37
|
+
<div class="name-of-value-in-card-bold">3</div>
|
|
38
|
+
</div>
|
|
39
|
+
|
|
40
|
+
<!-- LTE stats -->
|
|
41
|
+
<div class="LTE"><div class="bytes">1.5 GB</div><div class="bytes">256 MB</div></div>
|
|
42
|
+
<div class="Ethernet"><div class="bytes">256 MB</div><div class="bytes">1.5 GB</div></div>
|
|
43
|
+
|
|
44
|
+
<!-- APNs card -->
|
|
45
|
+
<div class="apns-section">
|
|
46
|
+
<div></div><div></div>
|
|
47
|
+
<div>internet</div><div>10.0.0.2</div><div>::</div>
|
|
48
|
+
</div>
|
|
49
|
+
|
|
50
|
+
<!-- Available cells grid (*x6) -->
|
|
51
|
+
<div id="available-cell-id-0">470</div>
|
|
52
|
+
<div id="available-earfcn-0">150</div>
|
|
53
|
+
<div id="rsrp-0">-56</div>
|
|
54
|
+
<div id="rsrq-0">-14</div>
|
|
55
|
+
<div id="rssi-0">-31</div>
|
|
56
|
+
<div id="sinr-0">30</div>
|
|
57
|
+
</html>
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_parse_snapshot_extracts_full_status_payload():
|
|
62
|
+
snapshot = parse_snapshot(HTML)
|
|
63
|
+
|
|
64
|
+
assert snapshot.device.model == "ODU - Multiband - 4G05-B"
|
|
65
|
+
assert snapshot.device.software_version == "FASTMILE2_D020110B83T0101M01E0153S"
|
|
66
|
+
assert snapshot.device.serial_number == "FSH22010081D"
|
|
67
|
+
assert snapshot.device.imei == "356088101152952"
|
|
68
|
+
assert snapshot.device.imsi == "260032773115333"
|
|
69
|
+
assert snapshot.device.mac == "DC:8D:8A:97:47:C2"
|
|
70
|
+
assert snapshot.device.lock_status == "Normal"
|
|
71
|
+
|
|
72
|
+
assert snapshot.lte.ca.enb == 291067
|
|
73
|
+
assert snapshot.lte.ca.cid == 53
|
|
74
|
+
assert snapshot.lte.ca.dl_bands == [3, 1]
|
|
75
|
+
assert snapshot.lte.ca.ul_bands == [3]
|
|
76
|
+
|
|
77
|
+
assert snapshot.lte.active[0].pci == 131
|
|
78
|
+
assert snapshot.lte.active[0].earfcn == 1725
|
|
79
|
+
assert snapshot.lte.active[0].cell_type == "CellPrimary"
|
|
80
|
+
assert snapshot.lte.active[0].rsrp == -106
|
|
81
|
+
assert snapshot.lte.active[0].rsrq == -13
|
|
82
|
+
assert snapshot.lte.active[0].rssi == -74
|
|
83
|
+
assert snapshot.lte.active[0].sinr == 6
|
|
84
|
+
|
|
85
|
+
assert snapshot.lte.active[1].pci == 311
|
|
86
|
+
assert snapshot.lte.active[1].cell_type == "CellSecondary"
|
|
87
|
+
|
|
88
|
+
assert snapshot.lte.available[0].pci == 470
|
|
89
|
+
assert snapshot.lte.available[0].earfcn == 150
|
|
90
|
+
assert snapshot.lte.available[0].rsrp == -56
|
|
91
|
+
assert snapshot.lte.available[0].rsrq == -14
|
|
92
|
+
assert snapshot.lte.available[0].rssi == -31
|
|
93
|
+
assert snapshot.lte.available[0].sinr == 30
|
|
94
|
+
|
|
95
|
+
assert snapshot.apns[0].name == "internet"
|
|
96
|
+
assert snapshot.apns[0].ipv4 == "10.0.0.2"
|
|
97
|
+
assert snapshot.apns[0].ipv6 is None
|
|
98
|
+
|
|
99
|
+
assert snapshot.data.eth.download.unit == "MB"
|
|
100
|
+
assert snapshot.data.eth.download.val_gb == 0.256
|
|
101
|
+
assert snapshot.data.lte.upload.unit == "MB"
|