repeaterbook 0.1.0__tar.gz → 0.2.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.
Files changed (49) hide show
  1. {repeaterbook-0.1.0 → repeaterbook-0.2.0}/PKG-INFO +1 -1
  2. repeaterbook-0.2.0/docs/CHANGELOG.md +36 -0
  3. repeaterbook-0.2.0/playground/.gitignore +3 -0
  4. repeaterbook-0.2.0/playground/examples.ipynb +107 -0
  5. {repeaterbook-0.1.0 → repeaterbook-0.2.0}/pyproject.toml +1 -1
  6. repeaterbook-0.2.0/src/repeaterbook/__init__.py +11 -0
  7. repeaterbook-0.2.0/src/repeaterbook/database.py +74 -0
  8. {repeaterbook-0.1.0 → repeaterbook-0.2.0}/src/repeaterbook/models.py +17 -17
  9. repeaterbook-0.2.0/src/repeaterbook/queries.py +113 -0
  10. {repeaterbook-0.1.0 → repeaterbook-0.2.0}/src/repeaterbook/services.py +8 -91
  11. {repeaterbook-0.1.0 → repeaterbook-0.2.0}/src/repeaterbook/utils.py +24 -9
  12. {repeaterbook-0.1.0 → repeaterbook-0.2.0}/uv.lock +1 -1
  13. repeaterbook-0.1.0/docs/CHANGELOG.md +0 -12
  14. repeaterbook-0.1.0/playground/.gitignore +0 -2
  15. repeaterbook-0.1.0/src/repeaterbook/__init__.py +0 -1
  16. {repeaterbook-0.1.0 → repeaterbook-0.2.0}/.all-contributorsrc +0 -0
  17. {repeaterbook-0.1.0 → repeaterbook-0.2.0}/.cruft.json +0 -0
  18. {repeaterbook-0.1.0 → repeaterbook-0.2.0}/.editorconfig +0 -0
  19. {repeaterbook-0.1.0 → repeaterbook-0.2.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  20. {repeaterbook-0.1.0 → repeaterbook-0.2.0}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  21. {repeaterbook-0.1.0 → repeaterbook-0.2.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  22. {repeaterbook-0.1.0 → repeaterbook-0.2.0}/.github/workflows/ci.yml +0 -0
  23. {repeaterbook-0.1.0 → repeaterbook-0.2.0}/.github/workflows/codecov_action.yml +0 -0
  24. {repeaterbook-0.1.0 → repeaterbook-0.2.0}/.github/workflows/semantic-release.yml +0 -0
  25. {repeaterbook-0.1.0 → repeaterbook-0.2.0}/.gitignore +0 -0
  26. {repeaterbook-0.1.0 → repeaterbook-0.2.0}/.pre-commit-config.yaml +0 -0
  27. {repeaterbook-0.1.0 → repeaterbook-0.2.0}/.python-version +0 -0
  28. {repeaterbook-0.1.0 → repeaterbook-0.2.0}/.python-versions +0 -0
  29. {repeaterbook-0.1.0 → repeaterbook-0.2.0}/.readthedocs.yaml +0 -0
  30. {repeaterbook-0.1.0 → repeaterbook-0.2.0}/LICENSE +0 -0
  31. {repeaterbook-0.1.0 → repeaterbook-0.2.0}/asv.conf.json +0 -0
  32. {repeaterbook-0.1.0 → repeaterbook-0.2.0}/benchmarks/__init__.py +0 -0
  33. {repeaterbook-0.1.0 → repeaterbook-0.2.0}/benchmarks/benchmarks.py +0 -0
  34. {repeaterbook-0.1.0 → repeaterbook-0.2.0}/docs/CODE_OF_CONDUCT.md +0 -0
  35. {repeaterbook-0.1.0 → repeaterbook-0.2.0}/docs/CONTRIBUTING.md +0 -0
  36. {repeaterbook-0.1.0 → repeaterbook-0.2.0}/docs/Makefile +0 -0
  37. {repeaterbook-0.1.0 → repeaterbook-0.2.0}/docs/README.md +0 -0
  38. {repeaterbook-0.1.0 → repeaterbook-0.2.0}/docs/__init__.py +0 -0
  39. {repeaterbook-0.1.0 → repeaterbook-0.2.0}/docs/_static/.gitignore +0 -0
  40. {repeaterbook-0.1.0 → repeaterbook-0.2.0}/docs/_templates/.gitignore +0 -0
  41. {repeaterbook-0.1.0 → repeaterbook-0.2.0}/docs/conf.py +0 -0
  42. {repeaterbook-0.1.0 → repeaterbook-0.2.0}/docs/index.rst +0 -0
  43. {repeaterbook-0.1.0 → repeaterbook-0.2.0}/docs/make.bat +0 -0
  44. {repeaterbook-0.1.0 → repeaterbook-0.2.0}/docs/reference.md +0 -0
  45. {repeaterbook-0.1.0 → repeaterbook-0.2.0}/docs/wordlist.txt +0 -0
  46. {repeaterbook-0.1.0 → repeaterbook-0.2.0}/noxfile.py +0 -0
  47. {repeaterbook-0.1.0 → repeaterbook-0.2.0}/src/repeaterbook/py.typed +0 -0
  48. {repeaterbook-0.1.0 → repeaterbook-0.2.0}/tests/__init__.py +0 -0
  49. {repeaterbook-0.1.0 → repeaterbook-0.2.0}/tests/test_repeaterbook.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: repeaterbook
3
- Version: 0.1.0
3
+ Version: 0.2.0
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
@@ -0,0 +1,36 @@
1
+ # CHANGELOG
2
+
3
+
4
+ ## v0.2.0 (2025-04-08)
5
+
6
+ ### Features
7
+
8
+ - Queries
9
+ ([`78972e5`](https://github.com/MicaelJarniac/repeaterbook/commit/78972e5cbdcd150dd9e6435d5dd5c759bb22f96b))
10
+
11
+
12
+ ## v0.1.1 (2025-04-03)
13
+
14
+ ### Bug Fixes
15
+
16
+ - Merge instead of add to local DB
17
+ ([`f6dfcbf`](https://github.com/MicaelJarniac/repeaterbook/commit/f6dfcbf242c9af07578d5a2e8e19047ee2db96b9))
18
+
19
+ ### Chores
20
+
21
+ - Dunder all as tuples
22
+ ([`2dfb808`](https://github.com/MicaelJarniac/repeaterbook/commit/2dfb8089fee6db5fc26b9d3ea986fe8f9ce86cd3))
23
+
24
+ - Simpler working dir default
25
+ ([`fecdd43`](https://github.com/MicaelJarniac/repeaterbook/commit/fecdd4353fb37f46d42de3b6da69d7d402b76742))
26
+
27
+
28
+ ## v0.1.0 (2025-03-31)
29
+
30
+ ### Features
31
+
32
+ - Initial release
33
+ ([`2a257dd`](https://github.com/MicaelJarniac/repeaterbook/commit/2a257ddaada98ffa6871e607a868aabf6556bae1))
34
+
35
+
36
+ ## v0.0.0 (2025-03-21)
@@ -0,0 +1,3 @@
1
+ *
2
+ !.gitignore
3
+ !examples.ipynb
@@ -0,0 +1,107 @@
1
+ {
2
+ "cells": [
3
+ {
4
+ "cell_type": "code",
5
+ "execution_count": null,
6
+ "metadata": {},
7
+ "outputs": [],
8
+ "source": [
9
+ "import pycountry\n",
10
+ "from anyio import Path\n",
11
+ "\n",
12
+ "from repeaterbook.models import ExportQuery\n",
13
+ "from repeaterbook.services import RepeaterBookAPI\n",
14
+ "\n",
15
+ "rb_api = RepeaterBookAPI(\n",
16
+ " app_name=\"RepeaterBook Python SDK\",\n",
17
+ " app_email=\"micael@jarniac.dev\",\n",
18
+ " working_dir=Path(),\n",
19
+ ")\n",
20
+ "repeaters = await rb_api.download(\n",
21
+ " query=ExportQuery(countries={pycountry.countries.get(name=\"Brazil\")})\n",
22
+ ")"
23
+ ]
24
+ },
25
+ {
26
+ "cell_type": "code",
27
+ "execution_count": null,
28
+ "metadata": {},
29
+ "outputs": [],
30
+ "source": [
31
+ "from repeaterbook import RepeaterBook\n",
32
+ "\n",
33
+ "rb = RepeaterBook(\n",
34
+ " working_dir=Path(),\n",
35
+ ")\n",
36
+ "rb.populate(repeaters)"
37
+ ]
38
+ },
39
+ {
40
+ "cell_type": "code",
41
+ "execution_count": null,
42
+ "metadata": {},
43
+ "outputs": [],
44
+ "source": [
45
+ "from haversine import Unit\n",
46
+ "from rich import print as pprint\n",
47
+ "\n",
48
+ "from repeaterbook import queries\n",
49
+ "from repeaterbook.models import Repeater\n",
50
+ "from repeaterbook.utils import LatLon, Radius\n",
51
+ "\n",
52
+ "radius = Radius(\n",
53
+ " origin=LatLon(\n",
54
+ " lat=-22.4000,\n",
55
+ " lon=-46.9000,\n",
56
+ " ),\n",
57
+ " distance=50,\n",
58
+ " unit=Unit.KILOMETERS,\n",
59
+ ")\n",
60
+ "\n",
61
+ "filtered_repeaters = queries.filter_radius(\n",
62
+ " rb.query(*queries.square(radius), Repeater.dmr_capable), radius\n",
63
+ ")\n",
64
+ "pprint(filtered_repeaters)"
65
+ ]
66
+ },
67
+ {
68
+ "cell_type": "code",
69
+ "execution_count": null,
70
+ "metadata": {},
71
+ "outputs": [],
72
+ "source": [
73
+ "from repeaterbook import queries\n",
74
+ "from repeaterbook.models import Status, Use\n",
75
+ "from repeaterbook.queries import Bands\n",
76
+ "\n",
77
+ "rb.query(\n",
78
+ " Repeater.dmr_capable | Repeater.analog_capable,\n",
79
+ " Repeater.operational_status == Status.ON_AIR,\n",
80
+ " Repeater.use_membership == Use.OPEN,\n",
81
+ " queries.band(Bands.M_2, Bands.CM_70),\n",
82
+ ")"
83
+ ]
84
+ }
85
+ ],
86
+ "metadata": {
87
+ "kernelspec": {
88
+ "display_name": ".venv",
89
+ "language": "python",
90
+ "name": "python3"
91
+ },
92
+ "language_info": {
93
+ "codemirror_mode": {
94
+ "name": "ipython",
95
+ "version": 3
96
+ },
97
+ "file_extension": ".py",
98
+ "mimetype": "text/x-python",
99
+ "name": "python",
100
+ "nbconvert_exporter": "python",
101
+ "pygments_lexer": "ipython3",
102
+ "version": "3.13.2"
103
+ }
104
+ },
105
+ "nbformat": 4,
106
+ "nbformat_minor": 2
107
+ }
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "repeaterbook"
3
- version = "0.1.0"
3
+ version = "0.2.0"
4
4
  description = "Python utility to work with data from RepeaterBook."
5
5
  authors = [
6
6
  {name = "Micael Jarniac", email = "micael@jarniac.dev"},
@@ -0,0 +1,11 @@
1
+ """Python utility to work with data from RepeaterBook."""
2
+
3
+ from __future__ import annotations
4
+
5
+ __all__: tuple[str, ...] = (
6
+ "Repeater",
7
+ "RepeaterBook",
8
+ )
9
+
10
+ from repeaterbook.database import RepeaterBook
11
+ from repeaterbook.models import Repeater
@@ -0,0 +1,74 @@
1
+ """Internal database module for RepeaterBook."""
2
+
3
+ from __future__ import annotations
4
+
5
+ __all__: tuple[str, ...] = ("RepeaterBook",)
6
+
7
+ from functools import cached_property
8
+ from typing import TYPE_CHECKING
9
+
10
+ import attrs
11
+ from anyio import Path
12
+ from loguru import logger
13
+ from sqlmodel import Session, SQLModel, create_engine, select
14
+
15
+ from repeaterbook.models import (
16
+ Repeater,
17
+ )
18
+
19
+ if TYPE_CHECKING: # pragma: no cover
20
+ from collections.abc import Iterable, Sequence
21
+
22
+ from sqlalchemy import Engine
23
+ from sqlalchemy.sql._typing import _ColumnExpressionArgument
24
+
25
+
26
+ @attrs.frozen
27
+ class RepeaterBook:
28
+ """RepeaterBook API client."""
29
+
30
+ working_dir: Path = attrs.Factory(Path)
31
+ database: str = "repeaterbook.db"
32
+
33
+ @property
34
+ def database_path(self) -> Path:
35
+ """Database path."""
36
+ return self.working_dir / self.database
37
+
38
+ @property
39
+ def database_uri(self) -> str:
40
+ """Database URI."""
41
+ return f"sqlite:///{self.database_path}"
42
+
43
+ @cached_property
44
+ def engine(self) -> Engine:
45
+ """Create database engine."""
46
+ return create_engine(self.database_uri)
47
+
48
+ def init_db(self) -> None:
49
+ """Initialize database."""
50
+ SQLModel.metadata.create_all(self.engine)
51
+
52
+ def populate(self, repeaters: Iterable[Repeater]) -> None:
53
+ """Populate internal database."""
54
+ self.init_db()
55
+
56
+ with Session(self.engine) as session:
57
+ for repeater in repeaters:
58
+ session.merge(repeater)
59
+ session.commit()
60
+
61
+ logger.info("Populated repeaters.")
62
+
63
+ def query(
64
+ self,
65
+ *where: _ColumnExpressionArgument[bool] | bool,
66
+ ) -> Sequence[Repeater]:
67
+ """Query the database."""
68
+ with Session(self.engine) as session:
69
+ statement = select(Repeater).where(*where)
70
+ repeaters = session.exec(statement).all()
71
+
72
+ logger.info(f"Found {len(repeaters)} repeaters.")
73
+
74
+ return repeaters
@@ -3,7 +3,7 @@
3
3
 
4
4
  from __future__ import annotations
5
5
 
6
- __all__: list[str] = [
6
+ __all__: tuple[str, ...] = (
7
7
  "Emergency",
8
8
  "EmergencyJSON",
9
9
  "ErrorJSON",
@@ -25,7 +25,7 @@ __all__: list[str] = [
25
25
  "UseJSON",
26
26
  "YesNoJSON",
27
27
  "ZeroOneJSON",
28
- ]
28
+ )
29
29
 
30
30
  from datetime import date
31
31
  from decimal import Decimal
@@ -83,22 +83,22 @@ class Repeater(SQLModel, table=True):
83
83
 
84
84
  state_id: str = Field(primary_key=True)
85
85
  repeater_id: int = Field(primary_key=True)
86
- frequency: Decimal
87
- input_frequency: Decimal
86
+ frequency: Decimal = Field(index=True)
87
+ input_frequency: Decimal = Field(index=True)
88
88
  pl_ctcss_uplink: str | None
89
89
  pl_ctcss_tsq_downlink: str | None
90
90
  location_nearest_city: str
91
91
  landmark: str | None
92
92
  region: str | None
93
- country: str | None
93
+ country: str | None = Field(index=True)
94
94
  county: str | None
95
95
  state: str | None
96
- latitude: Decimal
97
- longitude: Decimal
96
+ latitude: Decimal = Field(index=True)
97
+ longitude: Decimal = Field(index=True)
98
98
  precise: bool
99
99
  callsign: str | None
100
- use_membership: Use
101
- operational_status: Status
100
+ use_membership: Use = Field(index=True)
101
+ operational_status: Status = Field(index=True)
102
102
  ares: str | None
103
103
  races: str | None
104
104
  skywarn: str | None
@@ -108,23 +108,23 @@ class Repeater(SQLModel, table=True):
108
108
  echolink_node: str | None
109
109
  irlp_node: str | None
110
110
  wires_node: str | None
111
- dmr_capable: bool
111
+ dmr_capable: bool = Field(index=True)
112
112
  dmr_id: str | None
113
113
  dmr_color_code: str | None
114
- d_star_capable: bool
115
- nxdn_capable: bool
116
- apco_p_25_capable: bool
114
+ d_star_capable: bool = Field(index=True)
115
+ nxdn_capable: bool = Field(index=True)
116
+ apco_p_25_capable: bool = Field(index=True)
117
117
  p_25_nac: str | None
118
- m17_capable: bool
118
+ m17_capable: bool = Field(index=True)
119
119
  m17_can: str | None
120
- tetra_capable: bool
120
+ tetra_capable: bool = Field(index=True)
121
121
  tetra_mcc: str | None
122
122
  tetra_mnc: str | None
123
- yaesu_system_fusion_capable: bool
123
+ yaesu_system_fusion_capable: bool = Field(index=True)
124
124
  ysf_digital_id_uplink: str | None
125
125
  ysf_digital_id_downlink: str | None
126
126
  ysf_dsc: str | None
127
- analog_capable: bool
127
+ analog_capable: bool = Field(index=True)
128
128
  fm_bandwidth: Decimal | None
129
129
  notes: str | None
130
130
  last_update: date
@@ -0,0 +1,113 @@
1
+ """Queries."""
2
+
3
+ from __future__ import annotations
4
+
5
+ __all__: tuple[str, ...] = (
6
+ "Band",
7
+ "Bands",
8
+ "band",
9
+ "filter_radius",
10
+ "square",
11
+ )
12
+
13
+ from decimal import Decimal
14
+ from enum import Enum
15
+ from typing import TYPE_CHECKING, NamedTuple
16
+
17
+ from haversine import haversine # type: ignore[import-untyped]
18
+ from loguru import logger
19
+ from sqlmodel import or_
20
+
21
+ from repeaterbook.models import Repeater
22
+ from repeaterbook.utils import Radius, square_bounds
23
+
24
+ if TYPE_CHECKING: # pragma: no cover
25
+ from collections.abc import Iterable
26
+
27
+ from sqlalchemy.sql._typing import _ColumnExpressionArgument
28
+ from sqlalchemy.sql.elements import ColumnElement
29
+
30
+
31
+ def square(radius: Radius) -> tuple[_ColumnExpressionArgument[bool] | bool, ...]:
32
+ """Return a query for repeaters within a given square.
33
+
34
+ Note: This is a square, not a circle. Use `filter_radius` afterwards.
35
+ """
36
+ bounds = square_bounds(radius=radius)
37
+ return (
38
+ Repeater.latitude >= bounds.south,
39
+ Repeater.latitude <= bounds.north,
40
+ Repeater.longitude >= bounds.west,
41
+ Repeater.longitude <= bounds.east,
42
+ )
43
+
44
+
45
+ def filter_radius(
46
+ repeaters: Iterable[Repeater],
47
+ radius: Radius,
48
+ ) -> list[Repeater]:
49
+ """Filter repeaters within a given radius, and sort by distance.
50
+
51
+ Use after `square` to limit the number of repeaters to check.
52
+ This is a brute-force search, so it should be used with care.
53
+ """
54
+
55
+ class RepDist(NamedTuple):
56
+ """Repeater distance."""
57
+
58
+ repeater: Repeater
59
+ distance: float
60
+
61
+ rep_dists: list[RepDist] = []
62
+ for repeater in repeaters:
63
+ # Calculate the distance to the repeater.
64
+ distance = haversine(
65
+ radius.origin,
66
+ (repeater.latitude, repeater.longitude),
67
+ unit=radius.unit,
68
+ )
69
+
70
+ if distance <= radius.distance:
71
+ rep_dists.append(RepDist(repeater=repeater, distance=distance))
72
+
73
+ # Sort by distance.
74
+ rep_dists.sort(key=lambda x: x.distance)
75
+
76
+ # Log the number of repeaters found.
77
+ logger.info(
78
+ f"Found {len(rep_dists)} repeaters within {radius.distance} {radius.unit.name}."
79
+ )
80
+
81
+ # Convert to a list of repeaters.
82
+ return [rep_dist.repeater for rep_dist in rep_dists]
83
+
84
+
85
+ class Band(NamedTuple):
86
+ """Band."""
87
+
88
+ low: Decimal
89
+ high: Decimal
90
+
91
+
92
+ class Bands(Band, Enum):
93
+ """Bands."""
94
+
95
+ M_10 = Band(low=Decimal("28.0"), high=Decimal("29.7"))
96
+ M_6 = Band(low=Decimal("50.0"), high=Decimal("54.0"))
97
+ M_4 = Band(low=Decimal("70.0"), high=Decimal("72.0"))
98
+ M_2 = Band(low=Decimal("144.0"), high=Decimal("148.0"))
99
+ CM_70 = Band(low=Decimal("420.0"), high=Decimal("450.0"))
100
+ CM_33 = Band(low=Decimal("902.0"), high=Decimal("928.0"))
101
+ CM_23 = Band(low=Decimal("1240.0"), high=Decimal("1300.0"))
102
+ CM_13 = Band(low=Decimal("2300.0"), high=Decimal("2450.0"))
103
+ CM_3 = Band(low=Decimal("10000.0"), high=Decimal("10500.0"))
104
+
105
+
106
+ def band(*bands: Band) -> ColumnElement[bool]:
107
+ """Return a query for repeaters within a given band."""
108
+ return or_(
109
+ *(
110
+ (Repeater.frequency >= band.low) & (Repeater.frequency <= band.high)
111
+ for band in bands
112
+ )
113
+ )
@@ -2,29 +2,26 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- __all__: list[str] = [
5
+ __all__: tuple[str, ...] = (
6
6
  "BOOL_MAP",
7
7
  "STATUS_MAP",
8
8
  "USE_MAP",
9
- "RepeaterBook",
9
+ "RepeaterBookAPI",
10
10
  "fetch_json",
11
11
  "json_to_model",
12
- ]
12
+ )
13
13
 
14
14
  import asyncio
15
15
  import hashlib
16
16
  import json
17
17
  import time
18
18
  from datetime import date, timedelta
19
- from functools import cached_property
20
- from typing import TYPE_CHECKING, Any, ClassVar, Final, NamedTuple, cast
19
+ from typing import Any, ClassVar, Final, cast
21
20
 
22
21
  import aiohttp
23
22
  import attrs
24
23
  from anyio import Path
25
- from haversine import Unit, haversine # type: ignore[import-untyped]
26
24
  from loguru import logger
27
- from sqlmodel import Session, SQLModel, create_engine, select
28
25
  from tqdm import tqdm
29
26
  from yarl import URL
30
27
 
@@ -45,10 +42,6 @@ from repeaterbook.models import (
45
42
  Status,
46
43
  Use,
47
44
  )
48
- from repeaterbook.utils import LatLon, square_bounds
49
-
50
- if TYPE_CHECKING: # pragma: no cover
51
- from sqlalchemy import Engine
52
45
 
53
46
 
54
47
  async def fetch_json(
@@ -178,15 +171,14 @@ def json_to_model(j: RepeaterJSON, /) -> Repeater:
178
171
 
179
172
 
180
173
  @attrs.frozen
181
- class RepeaterBook:
174
+ class RepeaterBookAPI:
182
175
  """RepeaterBook API client."""
183
176
 
184
177
  base_url: URL = attrs.Factory(lambda: URL("https://repeaterbook.com"))
185
178
  app_name: str = "RepeaterBook Python SDK"
186
179
  app_email: str = "micael@jarniac.dev"
187
180
 
188
- working_dir: Path = attrs.Factory(lambda: Path())
189
- database: str = "repeaterbook.db"
181
+ working_dir: Path = attrs.Factory(Path)
190
182
 
191
183
  MAX_COUNT: ClassVar[int] = 3500
192
184
 
@@ -202,25 +194,6 @@ class RepeaterBook:
202
194
  await gitignore.write_text("*\n", encoding="utf-8")
203
195
  return cache
204
196
 
205
- @property
206
- def database_path(self) -> Path:
207
- """Database path."""
208
- return self.working_dir / self.database
209
-
210
- @property
211
- def database_uri(self) -> str:
212
- """Database URI."""
213
- return f"sqlite:///{self.database_path}"
214
-
215
- @cached_property
216
- def engine(self) -> Engine:
217
- """Create database engine."""
218
- return create_engine(self.database_uri)
219
-
220
- def init_db(self) -> None:
221
- """Initialize database."""
222
- SQLModel.metadata.create_all(self.engine)
223
-
224
197
  @property
225
198
  def url_api(self) -> URL:
226
199
  """RepeaterBook API base URL."""
@@ -327,70 +300,14 @@ class RepeaterBook:
327
300
  return await asyncio.gather(*tasks)
328
301
 
329
302
  async def download(self, query: ExportQuery) -> list[Repeater]:
330
- """Download data and populate internal database."""
303
+ """Download repeaters."""
331
304
  data = await self.export_multi_json(self.urls_export(query))
332
305
 
333
306
  results: list[RepeaterJSON] = []
334
307
  for export in data:
335
308
  results.extend(export["results"])
336
309
 
337
- self.init_db()
338
-
339
- repeaters: list[Repeater] = []
340
- with Session(self.engine) as session:
341
- for result in results:
342
- repeater = json_to_model(result)
343
- session.add(repeater)
344
- repeaters.append(repeater)
345
- session.commit()
310
+ repeaters = [json_to_model(result) for result in results]
346
311
 
347
312
  logger.info(f"Downloaded {len(repeaters)} repeaters.")
348
313
  return repeaters
349
-
350
- def find_nearest(
351
- self,
352
- latitude: float,
353
- longitude: float,
354
- *,
355
- max_distance: float = 80.0,
356
- unit: Unit = Unit.KILOMETERS,
357
- ) -> list[Repeater]:
358
- """Find repeaters within a given distance."""
359
-
360
- class RepDist(NamedTuple):
361
- """Repeater distance."""
362
-
363
- repeater: Repeater
364
- distance: float
365
-
366
- rep_dists: list[RepDist] = []
367
- with Session(self.engine) as session:
368
- # Calculate the square bounds for the given distance.
369
- bounds = square_bounds(LatLon(latitude, longitude), max_distance, unit=unit)
370
- statement = select(Repeater).where(
371
- Repeater.latitude >= bounds.south,
372
- Repeater.latitude <= bounds.north,
373
- Repeater.longitude >= bounds.west,
374
- Repeater.longitude <= bounds.east,
375
- )
376
- for repeater in session.exec(statement):
377
- # Calculate the distance to the repeater.
378
- distance = haversine(
379
- (latitude, longitude),
380
- (repeater.latitude, repeater.longitude),
381
- unit=unit,
382
- )
383
-
384
- if distance <= max_distance:
385
- rep_dists.append(RepDist(repeater=repeater, distance=distance))
386
-
387
- # Sort by distance.
388
- rep_dists.sort(key=lambda x: x.distance)
389
-
390
- # Log the number of repeaters found.
391
- logger.info(
392
- f"Found {len(rep_dists)} repeaters within {max_distance} {unit.name}."
393
- )
394
-
395
- # Convert to a list of repeaters.
396
- return [rep_dist.repeater for rep_dist in rep_dists]
@@ -2,11 +2,12 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- __all__: list[str] = [
5
+ __all__: tuple[str, ...] = (
6
6
  "LatLon",
7
+ "Radius",
7
8
  "SquareBounds",
8
9
  "square_bounds",
9
- ]
10
+ )
10
11
 
11
12
  from typing import NamedTuple
12
13
 
@@ -20,6 +21,14 @@ class LatLon(NamedTuple):
20
21
  lon: float
21
22
 
22
23
 
24
+ class Radius(NamedTuple):
25
+ """Radius."""
26
+
27
+ origin: LatLon
28
+ distance: float
29
+ unit: Unit = Unit.KILOMETERS
30
+
31
+
23
32
  class SquareBounds(NamedTuple):
24
33
  """Square bounds."""
25
34
 
@@ -29,14 +38,20 @@ class SquareBounds(NamedTuple):
29
38
  west: float
30
39
 
31
40
 
32
- def square_bounds(
33
- origin: LatLon, distance: float, unit: Unit = Unit.KILOMETERS
34
- ) -> SquareBounds:
41
+ def square_bounds(radius: Radius) -> SquareBounds:
35
42
  """Get square bounds around a point."""
36
- north = inverse_haversine(origin, distance, Direction.NORTH, unit=unit)[0]
37
- south = inverse_haversine(origin, distance, Direction.SOUTH, unit=unit)[0]
38
- east = inverse_haversine(origin, distance, Direction.EAST, unit=unit)[1]
39
- west = inverse_haversine(origin, distance, Direction.WEST, unit=unit)[1]
43
+ north = inverse_haversine(
44
+ radius.origin, radius.distance, Direction.NORTH, unit=radius.unit
45
+ )[0]
46
+ south = inverse_haversine(
47
+ radius.origin, radius.distance, Direction.SOUTH, unit=radius.unit
48
+ )[0]
49
+ east = inverse_haversine(
50
+ radius.origin, radius.distance, Direction.EAST, unit=radius.unit
51
+ )[1]
52
+ west = inverse_haversine(
53
+ radius.origin, radius.distance, Direction.WEST, unit=radius.unit
54
+ )[1]
40
55
 
41
56
  # If we've gone all the way around, things get messy. Just open it up to everything.
42
57
  if south > north:
@@ -2072,7 +2072,7 @@ wheels = [
2072
2072
 
2073
2073
  [[package]]
2074
2074
  name = "repeaterbook"
2075
- version = "0.1.0"
2075
+ version = "0.2.0"
2076
2076
  source = { editable = "." }
2077
2077
  dependencies = [
2078
2078
  { name = "aiohttp" },
@@ -1,12 +0,0 @@
1
- # CHANGELOG
2
-
3
-
4
- ## v0.1.0 (2025-03-31)
5
-
6
- ### Features
7
-
8
- - Initial release
9
- ([`2a257dd`](https://github.com/MicaelJarniac/repeaterbook/commit/2a257ddaada98ffa6871e607a868aabf6556bae1))
10
-
11
-
12
- ## v0.0.0 (2025-03-21)
@@ -1,2 +0,0 @@
1
- *
2
- !.gitignore
@@ -1 +0,0 @@
1
- """Python utility to work with data from RepeaterBook."""
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes