Skip to content

Commit 6098bcb

Browse files
authored
Merge pull request #17 from joostlek/auth
Add authentication support
2 parents d2bf70b + a4b9b7f commit 6098bcb

File tree

6 files changed

+136
-18
lines changed

6 files changed

+136
-18
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ from python_opensky import OpenSky, StatesResponse
3333
async def main() -> None:
3434
"""Show example of fetching all flight states."""
3535
async with OpenSky() as opensky:
36-
states: StatesResponse = await opensky.states()
36+
states: StatesResponse = await opensky.get_states()
3737
print(states)
3838

3939

examples/example.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
async def main() -> None:
99
"""Show example of fetching flight states from OpenSky."""
1010
async with OpenSky() as opensky:
11-
states: StatesResponse = await opensky.states()
11+
states: StatesResponse = await opensky.get_states()
1212
print(states)
1313

1414

src/python_opensky/exceptions.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,7 @@ class OpenSkyConnectionError(OpenSkyError):
1111

1212
class OpenSkyCoordinateError(OpenSkyError):
1313
"""OpenSky coordinate exception."""
14+
15+
16+
class OpenSkyUnauthenticatedError(OpenSkyError):
17+
"""OpenSky unauthenticated exception."""

src/python_opensky/opensky.py

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,16 @@
99
from typing import Any, cast
1010

1111
import async_timeout
12-
from aiohttp import ClientError, ClientResponseError, ClientSession
12+
from aiohttp import BasicAuth, ClientError, ClientResponseError, ClientSession
1313
from aiohttp.hdrs import METH_GET
1414
from yarl import URL
1515

1616
from .const import MAX_LATITUDE, MAX_LONGITUDE, MIN_LATITUDE, MIN_LONGITUDE
17-
from .exceptions import OpenSkyConnectionError, OpenSkyError
17+
from .exceptions import (
18+
OpenSkyConnectionError,
19+
OpenSkyError,
20+
OpenSkyUnauthenticatedError,
21+
)
1822
from .models import BoundingBox, StatesResponse
1923

2024

@@ -24,11 +28,22 @@ class OpenSky:
2428

2529
session: ClientSession | None = None
2630
request_timeout: int = 10
27-
api_host: str = "python_opensky-network.org"
31+
api_host: str = "opensky-network.org"
2832
opensky_credits: int = 400
2933
timezone = timezone.utc
3034
_close_session: bool = False
3135
_credit_usage: dict[datetime, int] = field(default_factory=dict)
36+
_auth: BasicAuth | None = None
37+
_contributing_user: bool = False
38+
39+
def authenticate(self, auth: BasicAuth, *, contributing_user: bool = False) -> None:
40+
"""Authenticate the user."""
41+
self._auth = auth
42+
self._contributing_user = contributing_user
43+
if contributing_user:
44+
self.opensky_credits = 8000
45+
else:
46+
self.opensky_credits = 4000
3247

3348
async def _request(
3449
self,
@@ -79,6 +94,7 @@ async def _request(
7994
response = await self.session.request(
8095
METH_GET,
8196
url.with_query(data),
97+
auth=self._auth,
8298
headers=headers,
8399
)
84100
response.raise_for_status()
@@ -105,7 +121,10 @@ async def _request(
105121

106122
return cast(dict[str, Any], await response.json())
107123

108-
async def states(self, bounding_box: BoundingBox | None = None) -> StatesResponse:
124+
async def get_states(
125+
self,
126+
bounding_box: BoundingBox | None = None,
127+
) -> StatesResponse:
109128
"""Retrieve state vectors for a given time."""
110129
credit_cost = 4
111130
params = {
@@ -132,6 +151,23 @@ async def states(self, bounding_box: BoundingBox | None = None) -> StatesRespons
132151

133152
return StatesResponse.parse_obj(data)
134153

154+
async def get_own_states(self, time: int = 0) -> StatesResponse:
155+
"""Retrieve state vectors from your own sensors."""
156+
if not self._auth:
157+
raise OpenSkyUnauthenticatedError
158+
params = {
159+
"time": time,
160+
}
161+
162+
data = await self._request("states/own", data=params)
163+
164+
data = {
165+
**data,
166+
"states": [self._convert_state(state) for state in data["states"]],
167+
}
168+
169+
return StatesResponse.parse_obj(data)
170+
135171
@staticmethod
136172
def calculate_credit_costs(bounding_box: BoundingBox) -> int:
137173
"""Calculate the amount of credits a request costs."""

tests/ruff.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ extend-select = [
77

88
extend-ignore = [
99
"S101", # Use of assert detected. As these are tests...
10+
"S106", # Detection of passwords...
1011
"SLF001", # Tests will access private/protected members...
1112
"TCH002", # pytest doesn't like this one...
1213
]

tests/test_states.py

Lines changed: 89 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33

44
import aiohttp
55
import pytest
6-
from aiohttp import ClientError
6+
from aiohttp import BasicAuth, ClientError
7+
from aiohttp.web_request import BaseRequest
78
from aresponses import Response, ResponsesMockServer
89

910
from python_opensky import (
@@ -15,10 +16,11 @@
1516
PositionSource,
1617
StatesResponse,
1718
)
19+
from python_opensky.exceptions import OpenSkyUnauthenticatedError
1820

1921
from . import load_fixture
2022

21-
OPENSKY_URL = "python_opensky-network.org"
23+
OPENSKY_URL = "opensky-network.org"
2224

2325

2426
async def test_states(
@@ -37,7 +39,7 @@ async def test_states(
3739
)
3840
async with aiohttp.ClientSession() as session:
3941
opensky = OpenSky(session=session)
40-
response: StatesResponse = await opensky.states()
42+
response: StatesResponse = await opensky.get_states()
4143
assert len(response.states) == 4
4244
assert response.time == 1683488744
4345
first_aircraft = response.states[0]
@@ -62,6 +64,29 @@ async def test_states(
6264
await opensky.close()
6365

6466

67+
async def test_own_states(
68+
aresponses: ResponsesMockServer,
69+
) -> None:
70+
"""Test retrieving own states."""
71+
aresponses.add(
72+
OPENSKY_URL,
73+
"/api/states/own",
74+
"GET",
75+
aresponses.Response(
76+
status=200,
77+
headers={"Content-Type": "application/json"},
78+
text=load_fixture("states.json"),
79+
),
80+
)
81+
async with aiohttp.ClientSession() as session:
82+
opensky = OpenSky(session=session)
83+
opensky.authenticate(BasicAuth(login="test", password="test"))
84+
response: StatesResponse = await opensky.get_own_states()
85+
assert len(response.states) == 4
86+
assert opensky.remaining_credits() == opensky.opensky_credits
87+
await opensky.close()
88+
89+
6590
async def test_states_with_bounding_box(
6691
aresponses: ResponsesMockServer,
6792
) -> None:
@@ -85,7 +110,7 @@ async def test_states_with_bounding_box(
85110
min_longitude=0,
86111
max_longitude=0,
87112
)
88-
await opensky.states(bounding_box=bounding_box)
113+
await opensky.get_states(bounding_box=bounding_box)
89114
await opensky.close()
90115

91116

@@ -105,8 +130,8 @@ async def test_credit_usage(
105130
)
106131
async with aiohttp.ClientSession() as session:
107132
opensky = OpenSky(session=session)
108-
await opensky.states()
109-
assert opensky.remaining_credits() == 396
133+
await opensky.get_states()
134+
assert opensky.remaining_credits() == opensky.opensky_credits - 4
110135
await opensky.close()
111136

112137

@@ -126,15 +151,15 @@ async def test_new_session(
126151
)
127152
async with OpenSky() as opensky:
128153
assert not opensky.session
129-
await opensky.states()
154+
await opensky.get_states()
130155
assert opensky.session
131156

132157

133158
async def test_timeout(aresponses: ResponsesMockServer) -> None:
134159
"""Test request timeout."""
135160

136161
# Faking a timeout by sleeping
137-
async def response_handler(_: aiohttp.ClientResponse) -> Response:
162+
async def response_handler(_: BaseRequest) -> Response:
138163
"""Response handler for this test."""
139164
await asyncio.sleep(2)
140165
return aresponses.Response(body="Goodmorning!")
@@ -149,14 +174,57 @@ async def response_handler(_: aiohttp.ClientResponse) -> Response:
149174
async with aiohttp.ClientSession() as session:
150175
opensky = OpenSky(session=session, request_timeout=1)
151176
with pytest.raises(OpenSkyConnectionError):
152-
assert await opensky.states()
177+
assert await opensky.get_states()
178+
await opensky.close()
179+
180+
181+
async def test_auth(aresponses: ResponsesMockServer) -> None:
182+
"""Test request authentication."""
183+
184+
def response_handler(request: BaseRequest) -> Response:
185+
"""Response handler for this test."""
186+
assert request.headers
187+
assert request.headers["Authorization"]
188+
assert request.headers["Authorization"] == "Basic dGVzdDp0ZXN0"
189+
return aresponses.Response(
190+
status=200,
191+
headers={"Content-Type": "application/json"},
192+
text=load_fixture("states.json"),
193+
)
194+
195+
aresponses.add(
196+
OPENSKY_URL,
197+
"/api/states/all",
198+
"GET",
199+
response_handler,
200+
)
201+
202+
async with aiohttp.ClientSession() as session:
203+
opensky = OpenSky(session=session)
204+
opensky.authenticate(BasicAuth(login="test", password="test"))
205+
await opensky.get_states()
206+
await opensky.close()
207+
208+
209+
async def test_user_credits() -> None:
210+
"""Test authenticated user credits."""
211+
async with aiohttp.ClientSession() as session:
212+
opensky = OpenSky(session=session)
213+
assert opensky.opensky_credits == 400
214+
opensky.authenticate(BasicAuth(login="test", password="test"))
215+
assert opensky.opensky_credits == 4000
216+
opensky.authenticate(
217+
BasicAuth(login="test", password="test"),
218+
contributing_user=True,
219+
)
220+
assert opensky.opensky_credits == 8000
153221
await opensky.close()
154222

155223

156224
async def test_request_error(aresponses: ResponsesMockServer) -> None:
157225
"""Test request error."""
158226

159-
async def response_handler(_: aiohttp.ClientResponse) -> Response:
227+
async def response_handler(_: BaseRequest) -> Response:
160228
"""Response handler for this test."""
161229
raise ClientError
162230

@@ -170,7 +238,7 @@ async def response_handler(_: aiohttp.ClientResponse) -> Response:
170238
async with aiohttp.ClientSession() as session:
171239
opensky = OpenSky(session=session)
172240
with pytest.raises(OpenSkyConnectionError):
173-
assert await opensky.states()
241+
assert await opensky.get_states()
174242
await opensky.close()
175243

176244

@@ -192,7 +260,16 @@ async def test_unexpected_server_response(
192260
async with aiohttp.ClientSession() as session:
193261
opensky = OpenSky(session=session)
194262
with pytest.raises(OpenSkyError):
195-
assert await opensky.states()
263+
assert await opensky.get_states()
264+
await opensky.close()
265+
266+
267+
async def test_unauthenticated_own_states() -> None:
268+
"""Test unauthenticated access to own states."""
269+
async with aiohttp.ClientSession() as session:
270+
opensky = OpenSky(session=session)
271+
with pytest.raises(OpenSkyUnauthenticatedError):
272+
assert await opensky.get_own_states()
196273
await opensky.close()
197274

198275

0 commit comments

Comments
 (0)