pyopensky 2.12__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.12 → pyopensky-2.15}/.github/workflows/run_tests.yml +7 -19
  2. {pyopensky-2.12 → pyopensky-2.15}/.gitignore +1 -0
  3. {pyopensky-2.12 → pyopensky-2.15}/PKG-INFO +4 -2
  4. {pyopensky-2.12 → pyopensky-2.15}/docs/source/credentials.rst +8 -24
  5. {pyopensky-2.12 → pyopensky-2.15}/pyproject.toml +2 -1
  6. {pyopensky-2.12 → pyopensky-2.15}/src/pyopensky/config.py +24 -8
  7. {pyopensky-2.12 → pyopensky-2.15}/src/pyopensky/rest.py +63 -12
  8. {pyopensky-2.12 → pyopensky-2.15}/src/pyopensky/schema.py +17 -0
  9. {pyopensky-2.12 → pyopensky-2.15}/src/pyopensky/trino.py +1 -0
  10. {pyopensky-2.12 → pyopensky-2.15}/src/pyopensky/tzdata.py +20 -3
  11. {pyopensky-2.12 → pyopensky-2.15}/tests/test_trino.py +0 -8
  12. pyopensky-2.15/uv.lock +4081 -0
  13. pyopensky-2.12/uv.lock +0 -3233
  14. {pyopensky-2.12 → pyopensky-2.15}/.github/dependabot.yml +0 -0
  15. {pyopensky-2.12 → pyopensky-2.15}/.github/renovate.json5 +0 -0
  16. {pyopensky-2.12 → pyopensky-2.15}/.github/workflows/build-docs.yml +0 -0
  17. {pyopensky-2.12 → pyopensky-2.15}/.github/workflows/pypi_publish.yml +0 -0
  18. {pyopensky-2.12 → pyopensky-2.15}/docs/.gitignore +0 -0
  19. {pyopensky-2.12 → pyopensky-2.15}/docs/Makefile +0 -0
  20. {pyopensky-2.12 → pyopensky-2.15}/docs/make.bat +0 -0
  21. {pyopensky-2.12 → pyopensky-2.15}/docs/source/_static/main_stylesheet.css +0 -0
  22. {pyopensky-2.12 → pyopensky-2.15}/docs/source/api_reference.rst +0 -0
  23. {pyopensky-2.12 → pyopensky-2.15}/docs/source/conf.py +0 -0
  24. {pyopensky-2.12 → pyopensky-2.15}/docs/source/index.rst +0 -0
  25. {pyopensky-2.12 → pyopensky-2.15}/docs/source/installation.rst +0 -0
  26. {pyopensky-2.12 → pyopensky-2.15}/docs/source/rest.rst +0 -0
  27. {pyopensky-2.12 → pyopensky-2.15}/docs/source/trino.rst +0 -0
  28. {pyopensky-2.12 → pyopensky-2.15}/license.txt +0 -0
  29. {pyopensky-2.12 → pyopensky-2.15}/readme.md +0 -0
  30. {pyopensky-2.12 → pyopensky-2.15}/scripts/decode_pymodes.py +0 -0
  31. {pyopensky-2.12 → pyopensky-2.15}/scripts/decode_rs1090.py +0 -0
  32. {pyopensky-2.12 → pyopensky-2.15}/src/pyopensky/__init__.py +0 -0
  33. {pyopensky-2.12 → pyopensky-2.15}/src/pyopensky/api.py +0 -0
  34. {pyopensky-2.12 → pyopensky-2.15}/src/pyopensky/py.typed +0 -0
  35. {pyopensky-2.12 → pyopensky-2.15}/src/pyopensky/s3.py +0 -0
  36. {pyopensky-2.12 → pyopensky-2.15}/src/pyopensky/time.py +0 -0
  37. {pyopensky-2.12 → pyopensky-2.15}/tests/test_rest.py +0 -0
  38. {pyopensky-2.12 → pyopensky-2.15}/tests/test_s3.py +0 -0
  39. {pyopensky-2.12 → pyopensky-2.15}/tests/test_tzdata.py +0 -0
@@ -36,31 +36,19 @@ jobs:
36
36
  with:
37
37
  fetch-depth: 0
38
38
 
39
- - name: Set up uv
40
- if: ${{ matrix.os == 'ubuntu-latest' || matrix.os == 'macos-latest' }}
41
- run: curl -LsSf https://astral.sh/uv/install.sh | sh
42
-
43
- - name: Set up uv
44
- if: ${{ matrix.os == 'windows-latest' }}
45
- run: irm https://astral.sh/uv/install.ps1 | iex
46
- shell: powershell
47
-
48
- - name: Cache folder for uv
49
- uses: actions/cache@v4
39
+ - name: Install uv
40
+ uses: astral-sh/setup-uv@v6
50
41
  with:
51
- path: /tmp/.uv-cache
52
- key: uv-${{ runner.os }}-${{ hashFiles('uv.lock') }}
53
- restore-keys: |
54
- uv-${{ runner.os }}-${{ hashFiles('uv.lock') }}
55
- uv-${{ runner.os }}
42
+ enable-cache: true
56
43
 
57
44
  - name: Cache folder for data
58
45
  uses: actions/cache@v4
59
46
  id: cache-folder
60
47
  with:
61
48
  path: |
62
- ${{ github.workspace }}/cache_opensky
63
- key: opensky-${{ runner.os }}-${{ hashFiles('uv.lock') }}
49
+ cache_opensky
50
+ enableCrossOsArchive: true
51
+ key: opensky-${{ hashFiles('uv.lock') }}
64
52
 
65
53
  - name: Install the project
66
54
  run: uv sync --all-extras --dev --python ${{ matrix.python-version }}
@@ -71,7 +59,7 @@ jobs:
71
59
  uv run ruff format --check
72
60
 
73
61
  - name: Run tests
74
- if: ${{ matrix.python-version == '3.12' || steps.cache-folder.outputs.cache-hit == 'true' }}
62
+ if: ${{ (matrix.python-version == '3.12' && matrix.os == 'ubuntu-latest') || steps.cache-folder.outputs.cache-hit == 'true' }}
75
63
  env:
76
64
  OPENSKY_USERNAME: ${{ secrets.OPENSKY_USERNAME }}
77
65
  OPENSKY_PASSWORD: ${{ secrets.OPENSKY_PASSWORD }}
@@ -25,6 +25,7 @@ var/
25
25
  *.egg-info/
26
26
  .installed.cfg
27
27
  *.egg
28
+ .idea/
28
29
 
29
30
  # PyInstaller
30
31
  # Usually these files are written by a python script from a template
@@ -1,9 +1,10 @@
1
- Metadata-Version: 2.3
1
+ Metadata-Version: 2.4
2
2
  Name: pyopensky
3
- Version: 2.12
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>
7
+ License-Expression: MIT
7
8
  Requires-Python: >=3.9
8
9
  Requires-Dist: appdirs>=1.4.4
9
10
  Requires-Dist: httpx>=0.27.2
@@ -11,6 +12,7 @@ Requires-Dist: minio>=7.2.8
11
12
  Requires-Dist: pandas>=2.2.2
12
13
  Requires-Dist: pyarrow>=17.0.0
13
14
  Requires-Dist: pyjwt>=2.9.0
15
+ Requires-Dist: python-dotenv>=1.0.1
14
16
  Requires-Dist: tqdm>=4.66.5
15
17
  Requires-Dist: trino[sqlalchemy]>=0.329.0
16
18
  Requires-Dist: typing-extensions>=4.12.2
@@ -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.12"
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" },
@@ -16,6 +16,7 @@ dependencies = [
16
16
  "pandas >= 2.2.2",
17
17
  "pyarrow >= 17.0.0",
18
18
  "pyjwt >= 2.9.0",
19
+ "python-dotenv>=1.0.1",
19
20
  "tqdm >= 4.66.5",
20
21
  "trino[sqlalchemy] >= 0.329.0",
21
22
  "typing-extensions >= 4.12.2",
@@ -6,33 +6,33 @@ import os
6
6
  from pathlib import Path
7
7
  from typing import TypedDict
8
8
 
9
+ import dotenv
9
10
  from appdirs import user_cache_dir, user_config_dir
10
11
 
11
12
  import pandas as pd
12
13
 
13
14
  _log = logging.getLogger(__name__)
15
+ dotenv.load_dotenv()
14
16
 
15
17
 
16
18
  DEFAULT_CONFIG = """
17
19
  [default]
18
20
  username =
19
21
  password =
22
+ client_id =
23
+ client_secret =
20
24
 
21
25
  # Uncomment the following if you have access to the s3 server
26
+ # [s3]
22
27
  # secret_key =
23
28
  # access_key =
24
29
 
25
- # If you need a different account on Trino, uncomment the following
26
- # [trino]
27
- # username =
28
- # password =
29
-
30
30
  [cache]
31
31
  ## You may set a different cache folder here if you have a preferred space
32
32
  ## for storing possibly large files.
33
33
  ## Default settings:
34
34
  ## - $HOME/.cache/opensky (Linux)
35
- ## - $HOME/Library/Caches/opensky (MacOS)
35
+ ## - $HOME/Library/Caches/opensky (MacOS) (unless XDG_CONFIG_HOME is set)
36
36
  ## - C:\\Users\\<username>\\AppData\\Local\\opensky\\Cache (Windows)
37
37
  ##
38
38
  # path =
@@ -44,7 +44,10 @@ password =
44
44
  purge = 90 days
45
45
  """
46
46
 
47
- opensky_config_dir = Path(user_config_dir("pyopensky"))
47
+ if (xdg_config := os.environ.get("XDG_CONFIG_HOME")) is not None:
48
+ opensky_config_dir = Path(xdg_config) / "pyopensky"
49
+ else:
50
+ opensky_config_dir = Path(user_config_dir("pyopensky"))
48
51
  opensky_config = configparser.ConfigParser()
49
52
 
50
53
  if (opensky_config_file := opensky_config_dir / "settings.conf").exists():
@@ -57,7 +60,10 @@ else:
57
60
  opensky_config_dir.mkdir(parents=True)
58
61
  opensky_config_file.write_text(DEFAULT_CONFIG)
59
62
 
60
- traffic_config_dir = Path(user_config_dir("traffic"))
63
+ if xdg_config is not None:
64
+ traffic_config_dir = Path(xdg_config) / "traffic"
65
+ else:
66
+ traffic_config_dir = Path(user_config_dir("traffic"))
61
67
  traffic_config = configparser.ConfigParser()
62
68
 
63
69
  if (traffic_config_file := traffic_config_dir / "traffic.conf").exists():
@@ -139,6 +145,16 @@ NAME_RESOLUTION: dict[str, Resolution] = {
139
145
  traffic_category="opensky",
140
146
  traffic_name="password",
141
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
+ ),
142
158
  "trino_username": dict(
143
159
  environment_variable="OPENSKY_TRINO_USERNAME",
144
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)
@@ -12,6 +12,8 @@ from pathlib import Path
12
12
 
13
13
  import httpx
14
14
 
15
+ rk = "Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders"
16
+
15
17
 
16
18
  def download_tzdata_windows(
17
19
  year: int = 2022,
@@ -19,9 +21,24 @@ def download_tzdata_windows(
19
21
  name: str = "tzdata",
20
22
  base_dir: None | Path = None,
21
23
  ) -> None:
22
- folder = (
23
- base_dir if base_dir else Path(os.path.expanduser("~")) / "Downloads"
24
- )
24
+ # This module only exists in Windows
25
+ from winreg import HKEY_CURRENT_USER, OpenKey, QueryValueEx # type: ignore
26
+
27
+ if base_dir is None:
28
+ conda_env_path = os.getenv("CONDA_PREFIX", None)
29
+ if conda_env_path:
30
+ base_dir = Path(conda_env_path) / "Lib" / "site-packages" / "tzdata"
31
+ else:
32
+ # This \o/ line finds the Downloads directory for the current user
33
+ with OpenKey(HKEY_CURRENT_USER, rk) as key:
34
+ download_path, _ = QueryValueEx(
35
+ key,
36
+ # don't ask what this uuid stands for...
37
+ "{374DE290-123F-4565-9164-39C4925E467B}",
38
+ )
39
+ base_dir = Path(download_path)
40
+
41
+ folder = base_dir
25
42
 
26
43
  if (folder / name).is_dir():
27
44
  return
@@ -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)