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.
Files changed (39) hide show
  1. {pyopensky-2.14 → pyopensky-2.15}/.github/workflows/run_tests.yml +1 -1
  2. {pyopensky-2.14 → pyopensky-2.15}/PKG-INFO +1 -1
  3. {pyopensky-2.14 → pyopensky-2.15}/docs/source/credentials.rst +8 -24
  4. {pyopensky-2.14 → pyopensky-2.15}/pyproject.toml +1 -1
  5. {pyopensky-2.14 → pyopensky-2.15}/src/pyopensky/config.py +13 -5
  6. {pyopensky-2.14 → pyopensky-2.15}/src/pyopensky/rest.py +63 -12
  7. {pyopensky-2.14 → pyopensky-2.15}/src/pyopensky/schema.py +17 -0
  8. {pyopensky-2.14 → pyopensky-2.15}/src/pyopensky/trino.py +1 -0
  9. {pyopensky-2.14 → pyopensky-2.15}/tests/test_trino.py +0 -8
  10. pyopensky-2.15/uv.lock +4081 -0
  11. pyopensky-2.14/uv.lock +0 -3827
  12. {pyopensky-2.14 → pyopensky-2.15}/.github/dependabot.yml +0 -0
  13. {pyopensky-2.14 → pyopensky-2.15}/.github/renovate.json5 +0 -0
  14. {pyopensky-2.14 → pyopensky-2.15}/.github/workflows/build-docs.yml +0 -0
  15. {pyopensky-2.14 → pyopensky-2.15}/.github/workflows/pypi_publish.yml +0 -0
  16. {pyopensky-2.14 → pyopensky-2.15}/.gitignore +0 -0
  17. {pyopensky-2.14 → pyopensky-2.15}/docs/.gitignore +0 -0
  18. {pyopensky-2.14 → pyopensky-2.15}/docs/Makefile +0 -0
  19. {pyopensky-2.14 → pyopensky-2.15}/docs/make.bat +0 -0
  20. {pyopensky-2.14 → pyopensky-2.15}/docs/source/_static/main_stylesheet.css +0 -0
  21. {pyopensky-2.14 → pyopensky-2.15}/docs/source/api_reference.rst +0 -0
  22. {pyopensky-2.14 → pyopensky-2.15}/docs/source/conf.py +0 -0
  23. {pyopensky-2.14 → pyopensky-2.15}/docs/source/index.rst +0 -0
  24. {pyopensky-2.14 → pyopensky-2.15}/docs/source/installation.rst +0 -0
  25. {pyopensky-2.14 → pyopensky-2.15}/docs/source/rest.rst +0 -0
  26. {pyopensky-2.14 → pyopensky-2.15}/docs/source/trino.rst +0 -0
  27. {pyopensky-2.14 → pyopensky-2.15}/license.txt +0 -0
  28. {pyopensky-2.14 → pyopensky-2.15}/readme.md +0 -0
  29. {pyopensky-2.14 → pyopensky-2.15}/scripts/decode_pymodes.py +0 -0
  30. {pyopensky-2.14 → pyopensky-2.15}/scripts/decode_rs1090.py +0 -0
  31. {pyopensky-2.14 → pyopensky-2.15}/src/pyopensky/__init__.py +0 -0
  32. {pyopensky-2.14 → pyopensky-2.15}/src/pyopensky/api.py +0 -0
  33. {pyopensky-2.14 → pyopensky-2.15}/src/pyopensky/py.typed +0 -0
  34. {pyopensky-2.14 → pyopensky-2.15}/src/pyopensky/s3.py +0 -0
  35. {pyopensky-2.14 → pyopensky-2.15}/src/pyopensky/time.py +0 -0
  36. {pyopensky-2.14 → pyopensky-2.15}/src/pyopensky/tzdata.py +0 -0
  37. {pyopensky-2.14 → pyopensky-2.15}/tests/test_rest.py +0 -0
  38. {pyopensky-2.14 → pyopensky-2.15}/tests/test_s3.py +0 -0
  39. {pyopensky-2.14 → pyopensky-2.15}/tests/test_tzdata.py +0 -0
@@ -37,7 +37,7 @@ jobs:
37
37
  fetch-depth: 0
38
38
 
39
39
  - name: Install uv
40
- uses: astral-sh/setup-uv@v5
40
+ uses: astral-sh/setup-uv@v6
41
41
  with:
42
42
  enable-cache: true
43
43
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyopensky
3
- Version: 2.14
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
- If you use the same login for the OpenSky website (therefore the REST API) and
25
- for Trino (it will be unified one day), just fill it here.
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).
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "pyopensky"
3
- version = "2.14"
3
+ version = "2.15"
4
4
  description = "A Python interface for OpenSky database"
5
5
  authors = [
6
6
  { name = "Xavier Olive", email = "git@xoolive.org" },
@@ -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.auth = cast(tuple[str, str], (username, password))
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, auth=self.auth)
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, auth=self.auth)
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.auth is not None) else "all"
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
- """Returns the route associated to a callsign."""
280
- json = self.get(
281
- f"https://opensky-network.org/api/routes?callsign={callsign}"
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
- return tuple(json["route"])
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
 
@@ -132,6 +132,7 @@ class Trino(OpenSkyDBAPI):
132
132
  user=trino_username,
133
133
  catalog="minio",
134
134
  schema="osky",
135
+ source="pyopensky",
135
136
  ),
136
137
  connect_args=dict(
137
138
  auth=JWTAuthentication(token)
@@ -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)