repeaterbook 0.2.0__tar.gz → 0.2.2__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.
- {repeaterbook-0.2.0 → repeaterbook-0.2.2}/PKG-INFO +5 -1
- {repeaterbook-0.2.0 → repeaterbook-0.2.2}/docs/CHANGELOG.md +27 -0
- {repeaterbook-0.2.0 → repeaterbook-0.2.2}/docs/README.md +4 -0
- {repeaterbook-0.2.0 → repeaterbook-0.2.2}/playground/examples.ipynb +1 -1
- {repeaterbook-0.2.0 → repeaterbook-0.2.2}/pyproject.toml +1 -1
- {repeaterbook-0.2.0 → repeaterbook-0.2.2}/src/repeaterbook/models.py +12 -0
- {repeaterbook-0.2.0 → repeaterbook-0.2.2}/src/repeaterbook/queries.py +3 -4
- {repeaterbook-0.2.0 → repeaterbook-0.2.2}/src/repeaterbook/services.py +77 -40
- repeaterbook-0.2.2/tests/test_api_format.py +115 -0
- {repeaterbook-0.2.0 → repeaterbook-0.2.2}/uv.lock +1377 -1382
- {repeaterbook-0.2.0 → repeaterbook-0.2.2}/.all-contributorsrc +0 -0
- {repeaterbook-0.2.0 → repeaterbook-0.2.2}/.cruft.json +0 -0
- {repeaterbook-0.2.0 → repeaterbook-0.2.2}/.editorconfig +0 -0
- {repeaterbook-0.2.0 → repeaterbook-0.2.2}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {repeaterbook-0.2.0 → repeaterbook-0.2.2}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {repeaterbook-0.2.0 → repeaterbook-0.2.2}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {repeaterbook-0.2.0 → repeaterbook-0.2.2}/.github/workflows/ci.yml +0 -0
- {repeaterbook-0.2.0 → repeaterbook-0.2.2}/.github/workflows/codecov_action.yml +0 -0
- {repeaterbook-0.2.0 → repeaterbook-0.2.2}/.github/workflows/semantic-release.yml +0 -0
- {repeaterbook-0.2.0 → repeaterbook-0.2.2}/.gitignore +0 -0
- {repeaterbook-0.2.0 → repeaterbook-0.2.2}/.pre-commit-config.yaml +0 -0
- {repeaterbook-0.2.0 → repeaterbook-0.2.2}/.python-version +0 -0
- {repeaterbook-0.2.0 → repeaterbook-0.2.2}/.python-versions +0 -0
- {repeaterbook-0.2.0 → repeaterbook-0.2.2}/.readthedocs.yaml +0 -0
- {repeaterbook-0.2.0 → repeaterbook-0.2.2}/LICENSE +0 -0
- {repeaterbook-0.2.0 → repeaterbook-0.2.2}/asv.conf.json +0 -0
- {repeaterbook-0.2.0 → repeaterbook-0.2.2}/benchmarks/__init__.py +0 -0
- {repeaterbook-0.2.0 → repeaterbook-0.2.2}/benchmarks/benchmarks.py +0 -0
- {repeaterbook-0.2.0 → repeaterbook-0.2.2}/docs/CODE_OF_CONDUCT.md +0 -0
- {repeaterbook-0.2.0 → repeaterbook-0.2.2}/docs/CONTRIBUTING.md +0 -0
- {repeaterbook-0.2.0 → repeaterbook-0.2.2}/docs/Makefile +0 -0
- {repeaterbook-0.2.0 → repeaterbook-0.2.2}/docs/__init__.py +0 -0
- {repeaterbook-0.2.0 → repeaterbook-0.2.2}/docs/_static/.gitignore +0 -0
- {repeaterbook-0.2.0 → repeaterbook-0.2.2}/docs/_templates/.gitignore +0 -0
- {repeaterbook-0.2.0 → repeaterbook-0.2.2}/docs/conf.py +0 -0
- {repeaterbook-0.2.0 → repeaterbook-0.2.2}/docs/index.rst +0 -0
- {repeaterbook-0.2.0 → repeaterbook-0.2.2}/docs/make.bat +0 -0
- {repeaterbook-0.2.0 → repeaterbook-0.2.2}/docs/reference.md +0 -0
- {repeaterbook-0.2.0 → repeaterbook-0.2.2}/docs/wordlist.txt +0 -0
- {repeaterbook-0.2.0 → repeaterbook-0.2.2}/noxfile.py +0 -0
- {repeaterbook-0.2.0 → repeaterbook-0.2.2}/playground/.gitignore +0 -0
- {repeaterbook-0.2.0 → repeaterbook-0.2.2}/src/repeaterbook/__init__.py +0 -0
- {repeaterbook-0.2.0 → repeaterbook-0.2.2}/src/repeaterbook/database.py +0 -0
- {repeaterbook-0.2.0 → repeaterbook-0.2.2}/src/repeaterbook/py.typed +0 -0
- {repeaterbook-0.2.0 → repeaterbook-0.2.2}/src/repeaterbook/utils.py +0 -0
- {repeaterbook-0.2.0 → repeaterbook-0.2.2}/tests/__init__.py +0 -0
- {repeaterbook-0.2.0 → repeaterbook-0.2.2}/tests/test_repeaterbook.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: repeaterbook
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: Python utility to work with data from RepeaterBook.
|
|
5
5
|
Project-URL: homepage, https://github.com/MicaelJarniac/repeaterbook
|
|
6
6
|
Project-URL: source, https://github.com/MicaelJarniac/repeaterbook
|
|
@@ -112,6 +112,10 @@ Python utility to work with data from RepeaterBook.
|
|
|
112
112
|
|
|
113
113
|
Read RepeaterBook's official [API documentation](https://www.repeaterbook.com/wiki/doku.php?id=api) for more information.
|
|
114
114
|
|
|
115
|
+
## Related Projects
|
|
116
|
+
- https://github.com/MicaelJarniac/opengd77
|
|
117
|
+
- https://github.com/MicaelJarniac/ogdrb
|
|
118
|
+
|
|
115
119
|
## See Also
|
|
116
120
|
- https://github.com/afourney/hamkit/tree/main/packages/repeaterbook
|
|
117
121
|
- https://github.com/desertblade/OpenGD77-Repeaterbook
|
|
@@ -1,8 +1,35 @@
|
|
|
1
1
|
# CHANGELOG
|
|
2
2
|
|
|
3
3
|
|
|
4
|
+
## v0.2.2 (2026-01-31)
|
|
5
|
+
|
|
6
|
+
### Bug Fixes
|
|
7
|
+
|
|
8
|
+
- Tolerate RepeaterBook API drift ([#2](https://github.com/MicaelJarniac/repeaterbook/pull/2),
|
|
9
|
+
[`665d78e`](https://github.com/MicaelJarniac/repeaterbook/commit/665d78ee38ca856a242b8f5f6289c441f00193a2))
|
|
10
|
+
|
|
11
|
+
* fix: tolerate RepeaterBook API drift (sponsor, NA fields, empty Use)
|
|
12
|
+
|
|
13
|
+
* refactor: simplify Region parsing (use .get)
|
|
14
|
+
|
|
15
|
+
* refactor: add b() helper for Yes/No + 1/0 fields
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
## v0.2.1 (2025-04-09)
|
|
19
|
+
|
|
20
|
+
### Chores
|
|
21
|
+
|
|
22
|
+
- Links
|
|
23
|
+
([`1d93cdb`](https://github.com/MicaelJarniac/repeaterbook/commit/1d93cdb5ae7dff17a6cb9943e66b2111f39617b5))
|
|
24
|
+
|
|
25
|
+
|
|
4
26
|
## v0.2.0 (2025-04-08)
|
|
5
27
|
|
|
28
|
+
### Bug Fixes
|
|
29
|
+
|
|
30
|
+
- Use `and_` for `square` query
|
|
31
|
+
([`9c09b5e`](https://github.com/MicaelJarniac/repeaterbook/commit/9c09b5eff8a2a4cef3dda91d5fa4d44001b0f241))
|
|
32
|
+
|
|
6
33
|
### Features
|
|
7
34
|
|
|
8
35
|
- Queries
|
|
@@ -81,6 +81,10 @@ Python utility to work with data from RepeaterBook.
|
|
|
81
81
|
|
|
82
82
|
Read RepeaterBook's official [API documentation](https://www.repeaterbook.com/wiki/doku.php?id=api) for more information.
|
|
83
83
|
|
|
84
|
+
## Related Projects
|
|
85
|
+
- https://github.com/MicaelJarniac/opengd77
|
|
86
|
+
- https://github.com/MicaelJarniac/ogdrb
|
|
87
|
+
|
|
84
88
|
## See Also
|
|
85
89
|
- https://github.com/afourney/hamkit/tree/main/packages/repeaterbook
|
|
86
90
|
- https://github.com/desertblade/OpenGD77-Repeaterbook
|
|
@@ -59,7 +59,7 @@
|
|
|
59
59
|
")\n",
|
|
60
60
|
"\n",
|
|
61
61
|
"filtered_repeaters = queries.filter_radius(\n",
|
|
62
|
-
" rb.query(
|
|
62
|
+
" rb.query(queries.square(radius), Repeater.dmr_capable), radius\n",
|
|
63
63
|
")\n",
|
|
64
64
|
"pprint(filtered_repeaters)"
|
|
65
65
|
]
|
|
@@ -165,6 +165,11 @@ EmergencyJSON: TypeAlias = Literal[
|
|
|
165
165
|
ServiceTypeJSON: TypeAlias = Literal["GMRS"]
|
|
166
166
|
|
|
167
167
|
|
|
168
|
+
# RepeaterBook has some variability between North America vs ROW exports.
|
|
169
|
+
# In practice, fields can appear/disappear (e.g. NA includes County/ARES/... and
|
|
170
|
+
# omits Region; ROW can include extra keys like "sponsor").
|
|
171
|
+
#
|
|
172
|
+
# Keep this TypedDict intentionally permissive for runtime robustness.
|
|
168
173
|
RepeaterJSON = TypedDict(
|
|
169
174
|
"RepeaterJSON",
|
|
170
175
|
{
|
|
@@ -179,6 +184,11 @@ RepeaterJSON = TypedDict(
|
|
|
179
184
|
"Region": str | None,
|
|
180
185
|
"State": str,
|
|
181
186
|
"Country": str,
|
|
187
|
+
"County": str,
|
|
188
|
+
"ARES": str,
|
|
189
|
+
"RACES": str,
|
|
190
|
+
"SKYWARN": str,
|
|
191
|
+
"CANWARN": str,
|
|
182
192
|
"Lat": str,
|
|
183
193
|
"Long": str,
|
|
184
194
|
"Precise": ZeroOneJSON,
|
|
@@ -206,7 +216,9 @@ RepeaterJSON = TypedDict(
|
|
|
206
216
|
"System Fusion": YesNoJSON,
|
|
207
217
|
"Notes": str,
|
|
208
218
|
"Last Update": str,
|
|
219
|
+
"sponsor": object,
|
|
209
220
|
},
|
|
221
|
+
total=False,
|
|
210
222
|
)
|
|
211
223
|
|
|
212
224
|
|
|
@@ -16,7 +16,7 @@ from typing import TYPE_CHECKING, NamedTuple
|
|
|
16
16
|
|
|
17
17
|
from haversine import haversine # type: ignore[import-untyped]
|
|
18
18
|
from loguru import logger
|
|
19
|
-
from sqlmodel import or_
|
|
19
|
+
from sqlmodel import and_, or_
|
|
20
20
|
|
|
21
21
|
from repeaterbook.models import Repeater
|
|
22
22
|
from repeaterbook.utils import Radius, square_bounds
|
|
@@ -24,17 +24,16 @@ from repeaterbook.utils import Radius, square_bounds
|
|
|
24
24
|
if TYPE_CHECKING: # pragma: no cover
|
|
25
25
|
from collections.abc import Iterable
|
|
26
26
|
|
|
27
|
-
from sqlalchemy.sql._typing import _ColumnExpressionArgument
|
|
28
27
|
from sqlalchemy.sql.elements import ColumnElement
|
|
29
28
|
|
|
30
29
|
|
|
31
|
-
def square(radius: Radius) ->
|
|
30
|
+
def square(radius: Radius) -> ColumnElement[bool]:
|
|
32
31
|
"""Return a query for repeaters within a given square.
|
|
33
32
|
|
|
34
33
|
Note: This is a square, not a circle. Use `filter_radius` afterwards.
|
|
35
34
|
"""
|
|
36
35
|
bounds = square_bounds(radius=radius)
|
|
37
|
-
return (
|
|
36
|
+
return and_(
|
|
38
37
|
Repeater.latitude >= bounds.south,
|
|
39
38
|
Repeater.latitude <= bounds.north,
|
|
40
39
|
Repeater.longitude >= bounds.west,
|
|
@@ -107,6 +107,7 @@ USE_MAP: Final = {
|
|
|
107
107
|
"OPEN": Use.OPEN,
|
|
108
108
|
"PRIVATE": Use.PRIVATE,
|
|
109
109
|
"CLOSED": Use.CLOSED,
|
|
110
|
+
"": Use.OPEN, # Some export payloads include empty Use; treat as OPEN.
|
|
110
111
|
}
|
|
111
112
|
|
|
112
113
|
STATUS_MAP: Final = {
|
|
@@ -125,54 +126,90 @@ def parse_date(date_str: str) -> date:
|
|
|
125
126
|
|
|
126
127
|
|
|
127
128
|
def json_to_model(j: RepeaterJSON, /) -> Repeater:
|
|
128
|
-
"""Converts a JSON object to a Repeater model.
|
|
129
|
+
"""Converts a JSON object to a Repeater model.
|
|
130
|
+
|
|
131
|
+
RepeaterBook export payloads vary slightly between endpoints.
|
|
132
|
+
|
|
133
|
+
- `exportROW.php` may include extra keys like `sponsor`.
|
|
134
|
+
- `export.php` (North America) includes keys like `County`/`ARES`/… and may omit
|
|
135
|
+
`Region`.
|
|
136
|
+
|
|
137
|
+
This function should be resilient to those differences.
|
|
138
|
+
"""
|
|
139
|
+
|
|
140
|
+
def s(key: str) -> str:
|
|
141
|
+
v = j.get(key, "")
|
|
142
|
+
if v is None:
|
|
143
|
+
return ""
|
|
144
|
+
return str(v)
|
|
145
|
+
|
|
146
|
+
def b(key: str, *, default: bool = False) -> bool:
|
|
147
|
+
"""Parse RepeaterBook boolean-ish fields.
|
|
148
|
+
|
|
149
|
+
RepeaterBook uses a mix of "Yes"/"No" strings and 1/0 ints.
|
|
150
|
+
Missing/unknown values fall back to `default`.
|
|
151
|
+
"""
|
|
152
|
+
return BOOL_MAP.get(j.get(key), default)
|
|
153
|
+
|
|
129
154
|
return Repeater.model_validate(
|
|
130
155
|
Repeater(
|
|
131
|
-
state_id=
|
|
132
|
-
repeater_id=j
|
|
133
|
-
frequency=
|
|
134
|
-
input_frequency=
|
|
135
|
-
pl_ctcss_uplink=
|
|
136
|
-
pl_ctcss_tsq_downlink=
|
|
137
|
-
location_nearest_city=
|
|
138
|
-
landmark=
|
|
139
|
-
region=j
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
156
|
+
state_id=s("State ID"),
|
|
157
|
+
repeater_id=int(j.get("Rptr ID", 0) or 0),
|
|
158
|
+
frequency=s("Frequency"),
|
|
159
|
+
input_frequency=s("Input Freq"),
|
|
160
|
+
pl_ctcss_uplink=s("PL") or None,
|
|
161
|
+
pl_ctcss_tsq_downlink=s("TSQ") or None,
|
|
162
|
+
location_nearest_city=s("Nearest City"),
|
|
163
|
+
landmark=s("Landmark") or None,
|
|
164
|
+
region=j.get("Region"),
|
|
165
|
+
country=s("Country") or None,
|
|
166
|
+
county=s("County") or None,
|
|
167
|
+
state=s("State") or None,
|
|
168
|
+
latitude=s("Lat"),
|
|
169
|
+
longitude=s("Long"),
|
|
170
|
+
precise=BOOL_MAP[j.get("Precise", 0)],
|
|
171
|
+
callsign=s("Callsign") or None,
|
|
172
|
+
use_membership=USE_MAP.get(s("Use"), Use.OPEN),
|
|
173
|
+
operational_status=(
|
|
174
|
+
STATUS_MAP[s("Operational Status")]
|
|
175
|
+
if s("Operational Status")
|
|
176
|
+
else Status.UNKNOWN
|
|
177
|
+
),
|
|
178
|
+
ares=s("ARES") or None,
|
|
179
|
+
races=s("RACES") or None,
|
|
180
|
+
skywarn=s("SKYWARN") or None,
|
|
181
|
+
canwarn=s("CANWARN") or None,
|
|
182
|
+
allstar_node=s("AllStar Node") or None,
|
|
183
|
+
echolink_node=s("EchoLink Node") or None,
|
|
184
|
+
irlp_node=s("IRLP Node") or None,
|
|
185
|
+
wires_node=s("Wires Node") or None,
|
|
186
|
+
analog_capable=b("FM Analog", default=False),
|
|
187
|
+
fm_bandwidth=s("FM Bandwidth").replace(" kHz", "") or None,
|
|
188
|
+
dmr_capable=b("DMR", default=False),
|
|
189
|
+
dmr_color_code=s("DMR Color Code") or None,
|
|
190
|
+
dmr_id=s("DMR ID") or None,
|
|
191
|
+
d_star_capable=b("D-Star", default=False),
|
|
192
|
+
nxdn_capable=b("NXDN", default=False),
|
|
193
|
+
apco_p_25_capable=b("APCO P-25", default=False),
|
|
194
|
+
p_25_nac=s("P-25 NAC") or None,
|
|
195
|
+
m17_capable=b("M17", default=False),
|
|
196
|
+
m17_can=s("M17 CAN") or None,
|
|
197
|
+
tetra_capable=b("Tetra", default=False),
|
|
198
|
+
tetra_mcc=s("Tetra MCC") or None,
|
|
199
|
+
tetra_mnc=s("Tetra MNC") or None,
|
|
200
|
+
yaesu_system_fusion_capable=b("System Fusion", default=False),
|
|
201
|
+
notes=s("Notes") or None,
|
|
202
|
+
last_update=parse_date(s("Last Update")),
|
|
169
203
|
)
|
|
170
204
|
)
|
|
171
205
|
|
|
172
206
|
|
|
173
207
|
@attrs.frozen
|
|
174
208
|
class RepeaterBookAPI:
|
|
175
|
-
"""RepeaterBook API client.
|
|
209
|
+
"""RepeaterBook API client.
|
|
210
|
+
|
|
211
|
+
Must read https://www.repeaterbook.com/wiki/doku.php?id=api before using.
|
|
212
|
+
"""
|
|
176
213
|
|
|
177
214
|
base_url: URL = attrs.Factory(lambda: URL("https://repeaterbook.com"))
|
|
178
215
|
app_name: str = "RepeaterBook Python SDK"
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""Regression tests for RepeaterBook API format drift.
|
|
2
|
+
|
|
3
|
+
We keep these tests offline by using minimal representative payload fragments.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from repeaterbook.services import json_to_model
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def test_json_to_model_accepts_row_payload_with_extra_keys() -> None:
|
|
12
|
+
"""ROW export recently started including extra keys like `sponsor`.
|
|
13
|
+
|
|
14
|
+
This should not break parsing.
|
|
15
|
+
"""
|
|
16
|
+
payload = {
|
|
17
|
+
"State ID": "BR",
|
|
18
|
+
"Rptr ID": 1065,
|
|
19
|
+
"Frequency": "53.750000",
|
|
20
|
+
"Input Freq": "52.15000",
|
|
21
|
+
"PL": "",
|
|
22
|
+
"TSQ": "",
|
|
23
|
+
"Nearest City": "Mateus Leme",
|
|
24
|
+
"Landmark": "",
|
|
25
|
+
"Region": None,
|
|
26
|
+
"State": "Brazil",
|
|
27
|
+
"Country": "Brazil",
|
|
28
|
+
"Lat": "-19.98950005",
|
|
29
|
+
"Long": "-44.43140030",
|
|
30
|
+
"Precise": 0,
|
|
31
|
+
"Callsign": "PY4RAP",
|
|
32
|
+
"Use": "OPEN",
|
|
33
|
+
"Operational Status": "On-air",
|
|
34
|
+
"AllStar Node": "0",
|
|
35
|
+
"EchoLink Node": "0",
|
|
36
|
+
"IRLP Node": "",
|
|
37
|
+
"Wires Node": "",
|
|
38
|
+
"FM Analog": "Yes",
|
|
39
|
+
"FM Bandwidth": "",
|
|
40
|
+
"DMR": "No",
|
|
41
|
+
"DMR Color Code": "",
|
|
42
|
+
"DMR ID": "",
|
|
43
|
+
"D-Star": "No",
|
|
44
|
+
"NXDN": "No",
|
|
45
|
+
"APCO P-25": "No",
|
|
46
|
+
"P-25 NAC": "",
|
|
47
|
+
"M17": "No",
|
|
48
|
+
"M17 CAN": "",
|
|
49
|
+
"Tetra": "No",
|
|
50
|
+
"Tetra MCC": "",
|
|
51
|
+
"Tetra MNC": "",
|
|
52
|
+
"System Fusion": "No",
|
|
53
|
+
"Notes": "",
|
|
54
|
+
"Last Update": "2025-01-01",
|
|
55
|
+
"sponsor": None,
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
rep = json_to_model(payload) # type: ignore[arg-type]
|
|
59
|
+
assert rep.country == "Brazil"
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def test_json_to_model_accepts_north_america_payload_without_region() -> None:
|
|
63
|
+
"""North America export includes County/ARES/... and may omit Region.
|
|
64
|
+
|
|
65
|
+
This used to raise KeyError; it should now parse.
|
|
66
|
+
"""
|
|
67
|
+
payload = {
|
|
68
|
+
"State ID": "06",
|
|
69
|
+
"Rptr ID": 1,
|
|
70
|
+
"Frequency": "146.880000",
|
|
71
|
+
"Input Freq": "146.280000",
|
|
72
|
+
"PL": "100.0",
|
|
73
|
+
"TSQ": "100.0",
|
|
74
|
+
"Nearest City": "Somewhere",
|
|
75
|
+
"Landmark": "",
|
|
76
|
+
# No Region key
|
|
77
|
+
"County": "SomeCounty",
|
|
78
|
+
"State": "California",
|
|
79
|
+
"Country": "United States",
|
|
80
|
+
"Lat": "34.0000",
|
|
81
|
+
"Long": "-118.0000",
|
|
82
|
+
"Precise": 1,
|
|
83
|
+
"Callsign": "W6TEST",
|
|
84
|
+
"Use": "OPEN",
|
|
85
|
+
"Operational Status": "On-air",
|
|
86
|
+
"ARES": "",
|
|
87
|
+
"RACES": "",
|
|
88
|
+
"SKYWARN": "",
|
|
89
|
+
"CANWARN": "",
|
|
90
|
+
"AllStar Node": "0",
|
|
91
|
+
"EchoLink Node": "0",
|
|
92
|
+
"IRLP Node": "",
|
|
93
|
+
"Wires Node": "",
|
|
94
|
+
"FM Analog": "Yes",
|
|
95
|
+
"FM Bandwidth": "",
|
|
96
|
+
"DMR": "No",
|
|
97
|
+
"DMR Color Code": "",
|
|
98
|
+
"DMR ID": "",
|
|
99
|
+
"D-Star": "No",
|
|
100
|
+
"NXDN": "No",
|
|
101
|
+
"APCO P-25": "No",
|
|
102
|
+
"P-25 NAC": "",
|
|
103
|
+
"M17": "No",
|
|
104
|
+
"M17 CAN": "",
|
|
105
|
+
"Tetra": "No",
|
|
106
|
+
"Tetra MCC": "",
|
|
107
|
+
"Tetra MNC": "",
|
|
108
|
+
"System Fusion": "No",
|
|
109
|
+
"Notes": "",
|
|
110
|
+
"Last Update": "2025-01-01",
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
rep = json_to_model(payload) # type: ignore[arg-type]
|
|
114
|
+
assert rep.region is None
|
|
115
|
+
assert rep.county == "SomeCounty"
|