pyopensky 2.14__tar.gz → 2.15__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.
- {pyopensky-2.14 → pyopensky-2.15}/.github/workflows/run_tests.yml +1 -1
- {pyopensky-2.14 → pyopensky-2.15}/PKG-INFO +1 -1
- {pyopensky-2.14 → pyopensky-2.15}/docs/source/credentials.rst +8 -24
- {pyopensky-2.14 → pyopensky-2.15}/pyproject.toml +1 -1
- {pyopensky-2.14 → pyopensky-2.15}/src/pyopensky/config.py +13 -5
- {pyopensky-2.14 → pyopensky-2.15}/src/pyopensky/rest.py +63 -12
- {pyopensky-2.14 → pyopensky-2.15}/src/pyopensky/schema.py +17 -0
- {pyopensky-2.14 → pyopensky-2.15}/src/pyopensky/trino.py +1 -0
- {pyopensky-2.14 → pyopensky-2.15}/tests/test_trino.py +0 -8
- pyopensky-2.15/uv.lock +4081 -0
- pyopensky-2.14/uv.lock +0 -3827
- {pyopensky-2.14 → pyopensky-2.15}/.github/dependabot.yml +0 -0
- {pyopensky-2.14 → pyopensky-2.15}/.github/renovate.json5 +0 -0
- {pyopensky-2.14 → pyopensky-2.15}/.github/workflows/build-docs.yml +0 -0
- {pyopensky-2.14 → pyopensky-2.15}/.github/workflows/pypi_publish.yml +0 -0
- {pyopensky-2.14 → pyopensky-2.15}/.gitignore +0 -0
- {pyopensky-2.14 → pyopensky-2.15}/docs/.gitignore +0 -0
- {pyopensky-2.14 → pyopensky-2.15}/docs/Makefile +0 -0
- {pyopensky-2.14 → pyopensky-2.15}/docs/make.bat +0 -0
- {pyopensky-2.14 → pyopensky-2.15}/docs/source/_static/main_stylesheet.css +0 -0
- {pyopensky-2.14 → pyopensky-2.15}/docs/source/api_reference.rst +0 -0
- {pyopensky-2.14 → pyopensky-2.15}/docs/source/conf.py +0 -0
- {pyopensky-2.14 → pyopensky-2.15}/docs/source/index.rst +0 -0
- {pyopensky-2.14 → pyopensky-2.15}/docs/source/installation.rst +0 -0
- {pyopensky-2.14 → pyopensky-2.15}/docs/source/rest.rst +0 -0
- {pyopensky-2.14 → pyopensky-2.15}/docs/source/trino.rst +0 -0
- {pyopensky-2.14 → pyopensky-2.15}/license.txt +0 -0
- {pyopensky-2.14 → pyopensky-2.15}/readme.md +0 -0
- {pyopensky-2.14 → pyopensky-2.15}/scripts/decode_pymodes.py +0 -0
- {pyopensky-2.14 → pyopensky-2.15}/scripts/decode_rs1090.py +0 -0
- {pyopensky-2.14 → pyopensky-2.15}/src/pyopensky/__init__.py +0 -0
- {pyopensky-2.14 → pyopensky-2.15}/src/pyopensky/api.py +0 -0
- {pyopensky-2.14 → pyopensky-2.15}/src/pyopensky/py.typed +0 -0
- {pyopensky-2.14 → pyopensky-2.15}/src/pyopensky/s3.py +0 -0
- {pyopensky-2.14 → pyopensky-2.15}/src/pyopensky/time.py +0 -0
- {pyopensky-2.14 → pyopensky-2.15}/src/pyopensky/tzdata.py +0 -0
- {pyopensky-2.14 → pyopensky-2.15}/tests/test_rest.py +0 -0
- {pyopensky-2.14 → pyopensky-2.15}/tests/test_s3.py +0 -0
- {pyopensky-2.14 → pyopensky-2.15}/tests/test_tzdata.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pyopensky
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.15
|
|
4
4
|
Summary: A Python interface for OpenSky database
|
|
5
5
|
Project-URL: Repository, https://github.com/open-aviation/pyopensky/
|
|
6
6
|
Author-email: Xavier Olive <git@xoolive.org>, Junzi Sun <j.sun-1@tudelft.nl>
|
|
@@ -20,9 +20,13 @@ is created, including the following content:
|
|
|
20
20
|
[default]
|
|
21
21
|
username =
|
|
22
22
|
password =
|
|
23
|
+
client_id =
|
|
24
|
+
client_secret =
|
|
23
25
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
+
|
|
27
|
+
The username and password is used to access the Trino database, while the
|
|
28
|
+
client ID and client secret are used to access the OpenSky REST API.
|
|
29
|
+
Log in to your OpenSky account and visit the `Account <https://opensky-network.org/my-opensky/account>`_ page in order to create a new API client and retrieve your ``client_id`` and ``client_secret``.
|
|
26
30
|
|
|
27
31
|
You will identify the folder where the ``settings.conf`` file is located:
|
|
28
32
|
|
|
@@ -32,26 +36,6 @@ You will identify the folder where the ``settings.conf`` file is located:
|
|
|
32
36
|
|
|
33
37
|
print(opensky_config_dir)
|
|
34
38
|
|
|
35
|
-
|
|
36
|
-
Different credentials per service
|
|
37
|
-
---------------------------------
|
|
38
|
-
|
|
39
|
-
If you want to specify different credentials for the REST API and for the Trino
|
|
40
|
-
database, use the following sections in the configuration file.
|
|
41
|
-
|
|
42
|
-
.. code::
|
|
43
|
-
|
|
44
|
-
[opensky] # REST API
|
|
45
|
-
username =
|
|
46
|
-
password =
|
|
47
|
-
|
|
48
|
-
[trino] # Trino database
|
|
49
|
-
username =
|
|
50
|
-
password =
|
|
51
|
-
|
|
52
|
-
If these sections are commented, the library will use the credentials from the
|
|
53
|
-
[default] section.
|
|
54
|
-
|
|
55
39
|
Fallback configuration
|
|
56
40
|
----------------------
|
|
57
41
|
|
|
@@ -61,5 +45,5 @@ following steps are performed in order:
|
|
|
61
45
|
- try to get the credentials from the `traffic <https://traffic-viz.github.io>`_
|
|
62
46
|
configuration file;
|
|
63
47
|
- get the credentials from ``$OPENSKY_USERNAME`` and ``$OPENSKY_PASSWORD``
|
|
64
|
-
environment variables;
|
|
65
|
-
- open a browser and log in on a dedicated webpage.
|
|
48
|
+
environment variables (``$OPENSKY_CLIENT_ID`` and ``$OPENSKY_CLIENT_SECRET`` for the API client);
|
|
49
|
+
- open a browser and log in on a dedicated webpage (for Trino).
|
|
@@ -19,16 +19,14 @@ DEFAULT_CONFIG = """
|
|
|
19
19
|
[default]
|
|
20
20
|
username =
|
|
21
21
|
password =
|
|
22
|
+
client_id =
|
|
23
|
+
client_secret =
|
|
22
24
|
|
|
23
25
|
# Uncomment the following if you have access to the s3 server
|
|
26
|
+
# [s3]
|
|
24
27
|
# secret_key =
|
|
25
28
|
# access_key =
|
|
26
29
|
|
|
27
|
-
# If you need a different account on Trino, uncomment the following
|
|
28
|
-
# [trino]
|
|
29
|
-
# username =
|
|
30
|
-
# password =
|
|
31
|
-
|
|
32
30
|
[cache]
|
|
33
31
|
## You may set a different cache folder here if you have a preferred space
|
|
34
32
|
## for storing possibly large files.
|
|
@@ -147,6 +145,16 @@ NAME_RESOLUTION: dict[str, Resolution] = {
|
|
|
147
145
|
traffic_category="opensky",
|
|
148
146
|
traffic_name="password",
|
|
149
147
|
),
|
|
148
|
+
"client_id": dict(
|
|
149
|
+
environment_variable="OPENSKY_CLIENT_ID",
|
|
150
|
+
opensky_category="default",
|
|
151
|
+
opensky_name="client_id",
|
|
152
|
+
),
|
|
153
|
+
"client_secret": dict(
|
|
154
|
+
environment_variable="OPENSKY_CLIENT_SECRET",
|
|
155
|
+
opensky_category="default",
|
|
156
|
+
opensky_name="client_secret",
|
|
157
|
+
),
|
|
150
158
|
"trino_username": dict(
|
|
151
159
|
environment_variable="OPENSKY_TRINO_USERNAME",
|
|
152
160
|
opensky_category="trino",
|
|
@@ -1,22 +1,31 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import datetime
|
|
3
4
|
import logging
|
|
4
5
|
import time
|
|
5
6
|
from datetime import timedelta
|
|
6
7
|
from json import JSONDecodeError
|
|
7
|
-
from typing import Any, cast
|
|
8
|
+
from typing import Any, TypedDict, cast
|
|
8
9
|
|
|
9
10
|
import httpx
|
|
10
11
|
|
|
11
12
|
import pandas as pd
|
|
12
13
|
|
|
13
14
|
from .api import HasBounds
|
|
14
|
-
from .config import password, username
|
|
15
|
+
from .config import client_id, client_secret, password, username
|
|
15
16
|
from .time import timelike, to_datetime
|
|
16
17
|
|
|
17
18
|
_log = logging.getLogger(__name__)
|
|
18
19
|
|
|
19
20
|
|
|
21
|
+
class TokenDict(TypedDict):
|
|
22
|
+
access_token: str
|
|
23
|
+
expires_in: int
|
|
24
|
+
token_type: str
|
|
25
|
+
# Added for convenience, not in the API response
|
|
26
|
+
expiration: datetime.datetime
|
|
27
|
+
|
|
28
|
+
|
|
20
29
|
class REST:
|
|
21
30
|
"""Wrapper to OpenSky REST API.
|
|
22
31
|
|
|
@@ -53,12 +62,54 @@ class REST:
|
|
|
53
62
|
def __init__(self) -> None:
|
|
54
63
|
self.username = username
|
|
55
64
|
self.password = password
|
|
56
|
-
self.
|
|
65
|
+
self.client_id = client_id
|
|
66
|
+
self.client_secret = client_secret
|
|
67
|
+
# self.auth = cast(tuple[str, str], (username, password))
|
|
68
|
+
self._token: None | TokenDict = None
|
|
57
69
|
|
|
58
70
|
self.client = httpx.Client()
|
|
59
71
|
|
|
72
|
+
@property
|
|
73
|
+
def token(self) -> None | TokenDict:
|
|
74
|
+
"""Returns the OpenSky token for the current user."""
|
|
75
|
+
|
|
76
|
+
if self._token is not None:
|
|
77
|
+
now = datetime.datetime.now(tz=datetime.timezone.utc)
|
|
78
|
+
if "expiration" in self._token and self._token["expiration"] > now:
|
|
79
|
+
return self._token
|
|
80
|
+
|
|
81
|
+
if self.client_id is None or self.client_secret is None:
|
|
82
|
+
return None
|
|
83
|
+
|
|
84
|
+
now = datetime.datetime.now(tz=datetime.timezone.utc)
|
|
85
|
+
url = "https://auth.opensky-network.org/auth/realms/opensky-network/protocol/openid-connect/token"
|
|
86
|
+
c = self.client.post(
|
|
87
|
+
url=url,
|
|
88
|
+
data=dict(
|
|
89
|
+
grant_type="client_credentials",
|
|
90
|
+
client_id=self.client_id,
|
|
91
|
+
client_secret=self.client_secret,
|
|
92
|
+
),
|
|
93
|
+
)
|
|
94
|
+
c.raise_for_status()
|
|
95
|
+
self._token = c.json()
|
|
96
|
+
assert self._token is not None
|
|
97
|
+
if "access_token" not in self._token:
|
|
98
|
+
raise RuntimeError("No access token found in response")
|
|
99
|
+
self._token["expiration"] = now + timedelta(
|
|
100
|
+
seconds=self._token["expires_in"]
|
|
101
|
+
)
|
|
102
|
+
return self._token
|
|
103
|
+
|
|
104
|
+
@property
|
|
105
|
+
def headers(self) -> dict[str, str]:
|
|
106
|
+
"""Returns the headers to use for OpenSky requests."""
|
|
107
|
+
if self.token is None:
|
|
108
|
+
return {}
|
|
109
|
+
return {"Authorization": f"Bearer {self.token['access_token']}"}
|
|
110
|
+
|
|
60
111
|
def get(self, query: str, retry: int = 5) -> Any:
|
|
61
|
-
c = self.client.get(query,
|
|
112
|
+
c = self.client.get(query, headers=self.headers)
|
|
62
113
|
try:
|
|
63
114
|
if limit := c.headers.get("X-Rate-Limit-Remaining", None):
|
|
64
115
|
limit = int(limit)
|
|
@@ -95,7 +146,7 @@ class REST:
|
|
|
95
146
|
async def async_get(
|
|
96
147
|
self, client: httpx.AsyncClient, query: str, retry: int = 5
|
|
97
148
|
) -> pd.DataFrame:
|
|
98
|
-
c = await client.get(query,
|
|
149
|
+
c = await client.get(query, headers=self.headers)
|
|
99
150
|
try:
|
|
100
151
|
if limit := c.headers.get("X-Rate-Limit-Remaining", None):
|
|
101
152
|
limit = int(limit)
|
|
@@ -170,7 +221,7 @@ class REST:
|
|
|
170
221
|
|
|
171
222
|
"""
|
|
172
223
|
|
|
173
|
-
what = "own" if (own and self.
|
|
224
|
+
what = "own" if (own and self.headers != {}) else "all"
|
|
174
225
|
|
|
175
226
|
if bounds is not None:
|
|
176
227
|
if isinstance(bounds, str):
|
|
@@ -275,13 +326,13 @@ class REST:
|
|
|
275
326
|
|
|
276
327
|
return df
|
|
277
328
|
|
|
278
|
-
def routes(self, callsign: str) -> tuple[str, str]:
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
329
|
+
# def routes(self, callsign: str) -> tuple[str, str]:
|
|
330
|
+
# """Returns the route associated to a callsign."""
|
|
331
|
+
# json = self.get(
|
|
332
|
+
# f"https://opensky-network.org/api/routes?callsign={callsign}"
|
|
333
|
+
# )
|
|
283
334
|
|
|
284
|
-
|
|
335
|
+
# return tuple(json["route"])
|
|
285
336
|
|
|
286
337
|
def aircraft(
|
|
287
338
|
self,
|
|
@@ -208,23 +208,40 @@ class Base(DeclarativeBase):
|
|
|
208
208
|
class StateVectorsData4(Base):
|
|
209
209
|
__tablename__ = "state_vectors_data4"
|
|
210
210
|
|
|
211
|
+
# The time at which the state vector was valid.
|
|
211
212
|
time: Mapped[pd.Timestamp]
|
|
213
|
+
# The unique 24-bit transponder ID assigned to the aircraft.
|
|
212
214
|
icao24: Mapped[Address]
|
|
215
|
+
# The last known latitude and longitude (WGS84 format).
|
|
213
216
|
lat: Mapped[float]
|
|
214
217
|
lon: Mapped[float]
|
|
218
|
+
# Speed over ground in meters per second.
|
|
215
219
|
velocity: Mapped[float]
|
|
220
|
+
# The direction of movement in degrees from geographic north.
|
|
216
221
|
heading: Mapped[float]
|
|
222
|
+
# Vertical speed in meters per second
|
|
223
|
+
# (positive = ascending, negative = descending).
|
|
217
224
|
vertrate: Mapped[float]
|
|
225
|
+
# The flight identifier broadcast by the aircraft.
|
|
218
226
|
callsign: Mapped[Callsign]
|
|
227
|
+
# Indicates if the aircraft is on the ground (true) or airborne (false).
|
|
219
228
|
onground: Mapped[bool]
|
|
229
|
+
# Special ATC indicators: alert squawk.
|
|
220
230
|
alert: Mapped[bool]
|
|
231
|
+
# Special ATC indicators: special position indicator.
|
|
221
232
|
spi: Mapped[bool]
|
|
233
|
+
# The 4-digit octal transponder code assigned by ATC.
|
|
222
234
|
squawk: Mapped[str]
|
|
235
|
+
# Barometric altitude measured by the aircraft in meters.
|
|
223
236
|
baroaltitude: Mapped[float]
|
|
237
|
+
# Geometric altitude determined by GNSS (GPS).
|
|
224
238
|
geoaltitude: Mapped[float]
|
|
239
|
+
# The timestamp of the last recorded position update.
|
|
225
240
|
lastposupdate: Mapped[float]
|
|
241
|
+
# The last time OpenSky received a signal from the aircraft.
|
|
226
242
|
lastcontact: Mapped[float]
|
|
227
243
|
serials: Mapped[List[int]] = mapped_column(ARRAY(Integer)) # TODO
|
|
244
|
+
# The start of the hour this data belongs to.
|
|
228
245
|
hour: Mapped[pd.Timestamp] = mapped_column(primary_key=True)
|
|
229
246
|
|
|
230
247
|
|
|
@@ -13,14 +13,6 @@ commercial_callsign = (
|
|
|
13
13
|
)
|
|
14
14
|
|
|
15
15
|
|
|
16
|
-
def test_alive() -> None:
|
|
17
|
-
df = trino.query(
|
|
18
|
-
"select * from state_vectors_data4 limit 5",
|
|
19
|
-
cached=False,
|
|
20
|
-
)
|
|
21
|
-
assert df.shape[0] == 5
|
|
22
|
-
|
|
23
|
-
|
|
24
16
|
def test_query() -> None:
|
|
25
17
|
res = trino.query(
|
|
26
18
|
select(FlightsData4)
|