pyopensky 2.17__tar.gz → 2.18__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. {pyopensky-2.17 → pyopensky-2.18}/.github/workflows/build-docs.yml +1 -1
  2. {pyopensky-2.17 → pyopensky-2.18}/.github/workflows/pypi_publish.yml +1 -1
  3. {pyopensky-2.17 → pyopensky-2.18}/.github/workflows/run_tests.yml +4 -4
  4. {pyopensky-2.17 → pyopensky-2.18}/.gitignore +1 -0
  5. {pyopensky-2.17 → pyopensky-2.18}/PKG-INFO +28 -1
  6. {pyopensky-2.17 → pyopensky-2.18}/docs/source/trino_advanced.rst +154 -3
  7. {pyopensky-2.17 → pyopensky-2.18}/pyproject.toml +1 -1
  8. {pyopensky-2.17 → pyopensky-2.18}/readme.md +27 -0
  9. pyopensky-2.18/scripts/benchmark_trino_routes.py +214 -0
  10. {pyopensky-2.17 → pyopensky-2.18}/src/pyopensky/api.py +9 -7
  11. {pyopensky-2.17 → pyopensky-2.18}/src/pyopensky/trino.py +196 -89
  12. pyopensky-2.18/tests/test_trino_offline.py +642 -0
  13. {pyopensky-2.17 → pyopensky-2.18}/uv.lock +101 -101
  14. {pyopensky-2.17 → pyopensky-2.18}/.gitattributes +0 -0
  15. {pyopensky-2.17 → pyopensky-2.18}/.github/dependabot.yml +0 -0
  16. {pyopensky-2.17 → pyopensky-2.18}/.github/renovate.json5 +0 -0
  17. {pyopensky-2.17 → pyopensky-2.18}/agents.md +0 -0
  18. {pyopensky-2.17 → pyopensky-2.18}/docs/.gitignore +0 -0
  19. {pyopensky-2.17 → pyopensky-2.18}/docs/Makefile +0 -0
  20. {pyopensky-2.17 → pyopensky-2.18}/docs/make.bat +0 -0
  21. {pyopensky-2.17 → pyopensky-2.18}/docs/source/_static/main_stylesheet.css +0 -0
  22. {pyopensky-2.17 → pyopensky-2.18}/docs/source/conf.py +0 -0
  23. {pyopensky-2.17 → pyopensky-2.18}/docs/source/credentials.rst +0 -0
  24. {pyopensky-2.17 → pyopensky-2.18}/docs/source/index.rst +0 -0
  25. {pyopensky-2.17 → pyopensky-2.18}/docs/source/installation.rst +0 -0
  26. {pyopensky-2.17 → pyopensky-2.18}/docs/source/rebuild.rst +0 -0
  27. {pyopensky-2.17 → pyopensky-2.18}/docs/source/rest.rst +0 -0
  28. {pyopensky-2.17 → pyopensky-2.18}/docs/source/trino.rst +0 -0
  29. {pyopensky-2.17 → pyopensky-2.18}/license.txt +0 -0
  30. {pyopensky-2.17 → pyopensky-2.18}/scripts/decode_pymodes.py +0 -0
  31. {pyopensky-2.17 → pyopensky-2.18}/scripts/decode_rs1090.py +0 -0
  32. {pyopensky-2.17 → pyopensky-2.18}/src/pyopensky/__init__.py +0 -0
  33. {pyopensky-2.17 → pyopensky-2.18}/src/pyopensky/config.py +0 -0
  34. {pyopensky-2.17 → pyopensky-2.18}/src/pyopensky/decoders/__init__.py +0 -0
  35. {pyopensky-2.17 → pyopensky-2.18}/src/pyopensky/decoders/base.py +0 -0
  36. {pyopensky-2.17 → pyopensky-2.18}/src/pyopensky/decoders/pymodes.py +0 -0
  37. {pyopensky-2.17 → pyopensky-2.18}/src/pyopensky/decoders/rs1090.py +0 -0
  38. {pyopensky-2.17 → pyopensky-2.18}/src/pyopensky/py.typed +0 -0
  39. {pyopensky-2.17 → pyopensky-2.18}/src/pyopensky/rebuild.py +0 -0
  40. {pyopensky-2.17 → pyopensky-2.18}/src/pyopensky/rest.py +0 -0
  41. {pyopensky-2.17 → pyopensky-2.18}/src/pyopensky/s3.py +0 -0
  42. {pyopensky-2.17 → pyopensky-2.18}/src/pyopensky/schema.py +0 -0
  43. {pyopensky-2.17 → pyopensky-2.18}/src/pyopensky/time.py +0 -0
  44. {pyopensky-2.17 → pyopensky-2.18}/src/pyopensky/tzdata.py +0 -0
  45. {pyopensky-2.17 → pyopensky-2.18}/tests/test_rebuild.py +0 -0
  46. {pyopensky-2.17 → pyopensky-2.18}/tests/test_rest.py +0 -0
  47. {pyopensky-2.17 → pyopensky-2.18}/tests/test_s3.py +0 -0
  48. {pyopensky-2.17 → pyopensky-2.18}/tests/test_trino.py +0 -0
  49. {pyopensky-2.17 → pyopensky-2.18}/tests/test_tzdata.py +0 -0
@@ -23,7 +23,7 @@ jobs:
23
23
  run: curl -LsSf https://astral.sh/uv/install.sh | sh
24
24
 
25
25
  - name: Set up Python
26
- uses: actions/setup-python@v7.0.0
26
+ uses: actions/setup-python@v7.1.0
27
27
  with:
28
28
  python-version: ${{ env.PYTHON_VERSION }}
29
29
 
@@ -15,7 +15,7 @@ jobs:
15
15
  run: curl -LsSf https://astral.sh/uv/install.sh | sh
16
16
 
17
17
  - name: Set up Python
18
- uses: actions/setup-python@v7.0.0
18
+ uses: actions/setup-python@v7.1.0
19
19
 
20
20
  - name: Cache folder for uv
21
21
  uses: actions/cache@v6
@@ -14,7 +14,7 @@ jobs:
14
14
  steps:
15
15
  - uses: actions/checkout@v7
16
16
 
17
- - uses: astral-sh/setup-uv@v10.1.0
17
+ - uses: astral-sh/setup-uv@v10.3.2
18
18
  with:
19
19
  enable-cache: true
20
20
 
@@ -29,7 +29,7 @@ jobs:
29
29
  steps:
30
30
  - uses: actions/checkout@v7
31
31
 
32
- - uses: astral-sh/setup-uv@v10.1.0
32
+ - uses: astral-sh/setup-uv@v10.3.2
33
33
  with:
34
34
  enable-cache: true
35
35
 
@@ -45,7 +45,7 @@ jobs:
45
45
  with:
46
46
  fetch-depth: 0
47
47
 
48
- - uses: astral-sh/setup-uv@v10.1.0
48
+ - uses: astral-sh/setup-uv@v10.3.2
49
49
  with:
50
50
  enable-cache: true
51
51
 
@@ -103,7 +103,7 @@ jobs:
103
103
  with:
104
104
  fetch-depth: 0
105
105
 
106
- - uses: astral-sh/setup-uv@v10.1.0
106
+ - uses: astral-sh/setup-uv@v10.3.2
107
107
  with:
108
108
  enable-cache: true
109
109
 
@@ -33,3 +33,4 @@ var/
33
33
  *.manifest
34
34
  *.spec
35
35
 
36
+ .secrets
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: pyopensky
3
- Version: 2.17
3
+ Version: 2.18
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>
@@ -138,5 +138,32 @@ trino.history(start, stop, *, callsign, icao24, bounds)
138
138
  trino.rebuild(start, stop, *, icao24, bounds)
139
139
  ```
140
140
 
141
+ For several routes in the same time window, use one query with exact directed
142
+ airport pairs:
143
+
144
+ ```python
145
+ df = trino.history(
146
+ "2025-01-01",
147
+ "2025-01-02",
148
+ routes=[("KJFK", "EGLL"), ("KMIA", "EIDW"), ("LFPG", "KJFK")],
149
+ selected_columns=("time", "icao24", "callsign", "lat", "lon"),
150
+ )
151
+ ```
152
+
153
+ The same `routes` filter is available in `flightlist()` for metadata queries and
154
+ `rawdata()` for raw messages.
155
+ `history()` clips trajectories to the requested time window, including flights
156
+ that cross midnight.
157
+
158
+ Optional Trino session settings apply to all queries on a client. For selective
159
+ flight joins whose filtered flight table fits in each worker's memory:
160
+
161
+ ```python
162
+ trino = Trino(session_properties={"join_distribution_type": "BROADCAST"})
163
+ ```
164
+
165
+ `Trino()` uses the server's default settings. Use `cached=False` on each call when
166
+ benchmarking query performance.
167
+
141
168
  > [!IMPORTANT]
142
169
  > Refer to the [documentation](https://mode-s.org/pyopensky) for advanced usage and parameter details.
@@ -70,6 +70,155 @@ Use ``with_only_columns()`` to reduce data transfer and improve query performanc
70
70
  - Schema attribute: ``StateVectorsData4.lat``
71
71
  - Qualified string: ``"StateVectorsData4.lat"`` or ``"FlightsData4.estdepartureairport"``
72
72
 
73
+ Configuring Trino sessions
74
+ ---------------------------
75
+
76
+ Pass ``session_properties`` to ``Trino`` to configure each connection opened by
77
+ that client. Names and values are strings. For example, a broadcast join can
78
+ reduce CPU and worker network traffic for selective flight queries:
79
+
80
+ .. code-block:: python
81
+
82
+ from pyopensky.trino import Trino
83
+
84
+ trino = Trino(
85
+ session_properties={"join_distribution_type": "BROADCAST"}
86
+ )
87
+ df = trino.history(
88
+ "2025-06-01 12:00",
89
+ "2025-06-01 13:00",
90
+ routes=[("EGLL", "KJFK"), ("LFPG", "KJFK"), ("EHAM", "KJFK")],
91
+ selected_columns=("time", "icao24", "callsign", "lat", "lon"),
92
+ )
93
+
94
+ ``Trino()`` uses the server's default session settings. Broadcast joins are
95
+ optional: the filtered build table must fit in each worker's memory. For large
96
+ flight selections, let the server choose the join strategy or choose another
97
+ session configuration. See `Trino join distribution selection
98
+ <https://trino.io/docs/current/optimizer/cost-based-optimizations.html#join-distribution-selection>`_.
99
+
100
+ Session properties apply to ``query()``, ``history()``, ``flightlist()``, and
101
+ ``rawdata()`` on that client. The constructor copies the supplied mapping.
102
+ Result-cache keys include configured properties, so different configurations do
103
+ not reuse each other's results. To measure query performance, pass
104
+ ``cached=False`` to every measured call.
105
+
106
+ Batching directed routes
107
+ ------------------------
108
+
109
+ Use ``routes`` to retrieve several directed airport pairs in one query for the
110
+ same time window. This avoids submitting one state-vector query per route.
111
+
112
+ .. code-block:: python
113
+
114
+ from pyopensky.trino import Trino
115
+
116
+ trino = Trino()
117
+ routes = [
118
+ ("KJFK", "EGLL"),
119
+ ("KMIA", "EIDW"),
120
+ ("LFPG", "KJFK"),
121
+ ]
122
+ df = trino.history(
123
+ "2025-01-01",
124
+ "2025-01-02",
125
+ routes=routes,
126
+ selected_columns=(
127
+ "time", "icao24", "callsign", "lat", "lon", "baroaltitude",
128
+ "FlightsData4.firstseen", "FlightsData4.lastseen",
129
+ "FlightsData4.estdepartureairport",
130
+ "FlightsData4.estarrivalairport",
131
+ ),
132
+ )
133
+
134
+ Each pair contains the estimated departure and arrival ICAO identifiers, in that
135
+ order. Only the listed pairs match. For example, ``[("KJFK", "EGLL"),
136
+ ("KMIA", "EIDW")]`` does not include KJFK to EIDW or the reverse directions.
137
+ Separate ``departure_airport`` and ``arrival_airport`` lists select every
138
+ combination between those lists.
139
+
140
+ Route identifiers match exactly; SQL wildcards are not expanded. An empty route
141
+ list selects no flights. Repeating a pair does not duplicate its results. Other
142
+ filters, such as ``icao24``, ``callsign``, bounds and airport filters, further
143
+ restrict the selected routes.
144
+
145
+ For long periods, loop over time windows and include all required routes in each
146
+ call. Select the columns needed for the analysis. Actual scan savings depend on
147
+ the Trino query plan and table layout.
148
+
149
+ If only flight metadata is needed, use the same route list with
150
+ ``trino.flightlist(start, stop, routes=routes)``. With routes alone,
151
+ ``flightlist()`` filters by ``lastseen``. Setting ``departure_airport`` also
152
+ selects its existing departure-time behavior.
153
+
154
+ .. note::
155
+
156
+ ``history()`` returns state vectors within ``start`` and ``stop``. These
157
+ timestamps do not select flights by their departure or arrival time. A flight
158
+ that crosses midnight can be partial in a daily result. ``time_buffer``
159
+ extends the bounds around each flight's ``firstseen`` and ``lastseen`` but
160
+ does not extend the requested state-vector window. Each call submits one
161
+ query; pyopensky does not split that window automatically.
162
+
163
+ Filtering flight metadata in history queries
164
+ --------------------------------------------
165
+
166
+ ``history()`` accepts ``FlightsData4`` conditions alongside state-vector
167
+ conditions. Flight columns refer to the flight table joined to the state vectors.
168
+ A flight condition also enables this join when no airport or route is specified.
169
+ Selecting flight columns also enables this join. Flight schema attributes can
170
+ be used directly in ``selected_columns``.
171
+
172
+ .. code-block:: python
173
+
174
+ from pyopensky.schema import FlightsData4, StateVectorsData4
175
+ from pyopensky.trino import Trino
176
+
177
+ df = Trino().history(
178
+ "2025-01-01",
179
+ "2025-01-02",
180
+ FlightsData4.estarrivalairporthorizdistance < 5000,
181
+ routes=[("KJFK", "EGLL"), ("LFPG", "KJFK")],
182
+ selected_columns=(
183
+ StateVectorsData4.time,
184
+ StateVectorsData4.icao24,
185
+ FlightsData4.estdepartureairport,
186
+ FlightsData4.estarrivalairport,
187
+ ),
188
+ )
189
+
190
+ Raw messages for directed routes
191
+ --------------------------------
192
+
193
+ ``rawdata()`` also accepts exact ``routes``. It joins raw messages to matching
194
+ flights by aircraft address and the flight's ``firstseen``/``lastseen`` interval,
195
+ then clips messages to the requested time window. FlightsData4 conditions and
196
+ extra columns use this joined flight table.
197
+
198
+ .. code-block:: python
199
+
200
+ from pyopensky.schema import FlightsData4, PositionData4
201
+ from pyopensky.trino import Trino
202
+
203
+ df = Trino().rawdata(
204
+ "2025-01-01",
205
+ "2025-01-02",
206
+ FlightsData4.estarrivalairporthorizdistance < 5000,
207
+ routes=[("KJFK", "EGLL"), ("LFPG", "KJFK")],
208
+ Table=PositionData4,
209
+ extra_columns=(FlightsData4.estdepartureairport,),
210
+ )
211
+
212
+ An empty route list selects no messages. Repeated pairs do not duplicate results.
213
+ Airport filters and ``icao24`` further restrict the selected routes. Flight
214
+ conditions or flight extra columns enable a flight join even without an airport
215
+ or route argument.
216
+
217
+ ``rawdata()`` supports one selection mode per call: flight metadata (airports,
218
+ routes, flight conditions or flight extra columns), geographic bounds, or
219
+ callsign. These modes cannot be combined. Conditions on the raw table can
220
+ further restrict any mode. Its existing raw-column output is preserved.
221
+
73
222
 
74
223
  Spatial queries with bounds
75
224
  ---------------------------
@@ -181,7 +330,8 @@ Filter by sensor serial numbers to get data from specific ground stations:
181
330
  Extending temporal range for airport queries
182
331
  --------------------------------------------
183
332
 
184
- Use ``time_buffer`` to include aircraft that departed/arrived slightly outside the time window:
333
+ Use ``time_buffer`` to include state vectors just before a flight's ``firstseen``
334
+ or after its ``lastseen``, within the requested time window:
185
335
 
186
336
  .. code-block:: python
187
337
 
@@ -189,7 +339,7 @@ Use ``time_buffer`` to include aircraft that departed/arrived slightly outside t
189
339
 
190
340
  trino = Trino()
191
341
 
192
- # Get flights with 30-minute buffer before/after time window
342
+ # Allow samples up to 30 minutes before/after each flight's time bounds
193
343
  df = trino.history(
194
344
  start="2024-03-16 09:00",
195
345
  stop="2024-03-16 11:00",
@@ -197,7 +347,8 @@ Use ``time_buffer`` to include aircraft that departed/arrived slightly outside t
197
347
  time_buffer="30m", # Also accepts "1h", "45min", etc.
198
348
  )
199
349
 
200
- This is useful when you want complete flight trajectories that may start or end outside your time range.
350
+ The result remains clipped to 09:00 through 11:00. To retrieve samples outside
351
+ that window, extend ``start`` or ``stop`` explicitly.
201
352
 
202
353
  Filtering with boolean expressions
203
354
  ----------------------------------
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "pyopensky"
3
- version = "2.17"
3
+ version = "2.18"
4
4
  description = "A Python interface for OpenSky database"
5
5
  authors = [{ name = "Xavier Olive", email = "git@xoolive.org" }, { name = "Junzi Sun", email = "j.sun-1@tudelft.nl" }]
6
6
  license = "MIT"
@@ -109,5 +109,32 @@ trino.history(start, stop, *, callsign, icao24, bounds)
109
109
  trino.rebuild(start, stop, *, icao24, bounds)
110
110
  ```
111
111
 
112
+ For several routes in the same time window, use one query with exact directed
113
+ airport pairs:
114
+
115
+ ```python
116
+ df = trino.history(
117
+ "2025-01-01",
118
+ "2025-01-02",
119
+ routes=[("KJFK", "EGLL"), ("KMIA", "EIDW"), ("LFPG", "KJFK")],
120
+ selected_columns=("time", "icao24", "callsign", "lat", "lon"),
121
+ )
122
+ ```
123
+
124
+ The same `routes` filter is available in `flightlist()` for metadata queries and
125
+ `rawdata()` for raw messages.
126
+ `history()` clips trajectories to the requested time window, including flights
127
+ that cross midnight.
128
+
129
+ Optional Trino session settings apply to all queries on a client. For selective
130
+ flight joins whose filtered flight table fits in each worker's memory:
131
+
132
+ ```python
133
+ trino = Trino(session_properties={"join_distribution_type": "BROADCAST"})
134
+ ```
135
+
136
+ `Trino()` uses the server's default settings. Use `cached=False` on each call when
137
+ benchmarking query performance.
138
+
112
139
  > [!IMPORTANT]
113
140
  > Refer to the [documentation](https://mode-s.org/pyopensky) for advanced usage and parameter details.
@@ -0,0 +1,214 @@
1
+ """Compare separate route queries at a Git revision with one routes query.
2
+
3
+ Run from the repository root, for example:
4
+ uv run --env-file .secrets scripts/benchmark_trino_routes.py \
5
+ --baseline-ref origin/master --output /tmp/trino-routes-benchmark.json
6
+
7
+ This runs live Trino queries. Every query uses cached=False and an isolated,
8
+ temporary result-cache directory. Server and storage caches are not controlled.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import argparse
14
+ import hashlib
15
+ import json
16
+ import platform
17
+ import subprocess
18
+ import time
19
+ import types
20
+ from contextlib import contextmanager
21
+ from importlib.metadata import version
22
+ from pathlib import Path
23
+ from tempfile import TemporaryDirectory
24
+ from typing import Any, Iterable, Iterator
25
+ from unittest.mock import patch
26
+
27
+ from sqlalchemy import CursorResult
28
+
29
+ import pandas as pd
30
+ import pyopensky.trino as current
31
+ from pyopensky.trino import Trino
32
+
33
+ COLUMNS = ("time", "icao24", "callsign", "lat", "lon", "baroaltitude")
34
+ DEFAULT_ROUTES = [("EGLL", "KJFK"), ("LFPG", "KJFK"), ("EHAM", "KJFK")]
35
+
36
+
37
+ @contextmanager
38
+ def instrument(client: Trino, records: list[dict[str, Any]]) -> Iterator[None]:
39
+ process_result = client.process_result
40
+
41
+ def record(
42
+ result: CursorResult[Any], batch_size: int = 50_000
43
+ ) -> Iterable[pd.DataFrame]:
44
+ cursor = result.cursor
45
+ yield from process_result(result, batch_size)
46
+ assert cursor is not None
47
+ records.append(dict(cursor.stats))
48
+
49
+ with patch.object(client, "process_result", record):
50
+ yield
51
+
52
+
53
+ def run(
54
+ client: Trino,
55
+ mode: str,
56
+ start: str,
57
+ stop: str,
58
+ routes: list[tuple[str, str]],
59
+ ) -> tuple[pd.DataFrame, float]:
60
+ frames = []
61
+ begin = time.perf_counter()
62
+ if mode == "before":
63
+ for departure, arrival in routes:
64
+ frame = client.history(
65
+ start,
66
+ stop,
67
+ departure_airport=departure,
68
+ arrival_airport=arrival,
69
+ selected_columns=COLUMNS,
70
+ cached=False,
71
+ )
72
+ if frame is not None:
73
+ frames.append(frame)
74
+ else:
75
+ frame = client.history(
76
+ start, stop, routes=routes, selected_columns=COLUMNS, cached=False
77
+ )
78
+ if frame is not None:
79
+ frames.append(frame)
80
+ result = pd.concat(frames, ignore_index=True) if frames else pd.DataFrame()
81
+ return result, time.perf_counter() - begin
82
+
83
+
84
+ def main() -> None:
85
+ parser = argparse.ArgumentParser(description=__doc__)
86
+ parser.add_argument("--baseline-ref", required=True)
87
+ parser.add_argument("--start", default="2025-06-01 12:00")
88
+ parser.add_argument("--stop", default="2025-06-01 13:00")
89
+ parser.add_argument("--route", action="append", help="DEPARTURE:ARRIVAL")
90
+ parser.add_argument("--rounds", type=int, default=2)
91
+ parser.add_argument(
92
+ "--after-session-property",
93
+ action="append",
94
+ default=[],
95
+ help="NAME=VALUE; configure the batched client only (repeatable)",
96
+ )
97
+ parser.add_argument("--output", type=Path, required=True)
98
+ args = parser.parse_args()
99
+ if args.rounds < 1:
100
+ parser.error("--rounds must be at least 1")
101
+ session_properties = {}
102
+ for setting in args.after_session_property:
103
+ name, separator, value = setting.partition("=")
104
+ if not separator or not name or not value:
105
+ parser.error(
106
+ "--after-session-property must have the form NAME=VALUE"
107
+ )
108
+ session_properties[name] = value
109
+ routes = DEFAULT_ROUTES
110
+ if args.route:
111
+ routes = []
112
+ for route in args.route:
113
+ parts = route.split(":")
114
+ if len(parts) != 2 or not all(parts):
115
+ parser.error("--route must have the form DEPARTURE:ARRIVAL")
116
+ routes.append((parts[0], parts[1]))
117
+ routes = list(dict.fromkeys(routes))
118
+
119
+ baseline_commit = subprocess.check_output(
120
+ ["git", "rev-parse", "--verify", f"{args.baseline_ref}^{{commit}}"],
121
+ text=True,
122
+ ).strip()
123
+ source = subprocess.check_output(
124
+ ["git", "show", f"{baseline_commit}:src/pyopensky/trino.py"], text=True
125
+ )
126
+ baseline = types.ModuleType("pyopensky._routes_benchmark_before")
127
+ baseline.__package__ = "pyopensky"
128
+ exec(
129
+ compile(source, f"{baseline_commit}:trino.py", "exec"),
130
+ baseline.__dict__,
131
+ )
132
+
133
+ report: dict[str, Any] = {
134
+ "baseline_commit": baseline_commit,
135
+ "after_source_sha256": hashlib.sha256(
136
+ Path(current.__file__).read_bytes()
137
+ ).hexdigest(),
138
+ "started_at_utc": pd.Timestamp.now(tz="UTC").isoformat(),
139
+ "start": args.start,
140
+ "stop": args.stop,
141
+ "timezone": "UTC",
142
+ "routes": routes,
143
+ "selected_columns": COLUMNS,
144
+ "cached": False,
145
+ "after_session_properties": session_properties,
146
+ "cache_directory": "isolated temporary directory",
147
+ "server_and_storage_caches": "not controlled",
148
+ "python": platform.python_version(),
149
+ "packages": {
150
+ name: version(name) for name in ("pyopensky", "trino", "sqlalchemy")
151
+ },
152
+ "runs": [],
153
+ }
154
+ with TemporaryDirectory(prefix="pyopensky-routes-benchmark-") as directory:
155
+ current.cache_path = Path(directory)
156
+ setattr(baseline, "cache_path", Path(directory))
157
+ after = Trino(session_properties=session_properties)
158
+ after.token() # Authenticate before timing either workload.
159
+ before = baseline.Trino()
160
+ before._token = after._token
161
+ clients = {"before": before, "after": after}
162
+ reference = None
163
+ for round_number in range(args.rounds):
164
+ order = (
165
+ ("before", "after")
166
+ if round_number % 2 == 0
167
+ else ("after", "before")
168
+ )
169
+ for mode in order:
170
+ records: list[dict[str, Any]] = []
171
+ client = clients[mode]
172
+ with instrument(client, records):
173
+ frame, seconds = run(
174
+ client, mode, args.start, args.stop, routes
175
+ )
176
+ canonical = (
177
+ frame.sort_values(list(COLUMNS)).reset_index(drop=True)
178
+ if len(frame)
179
+ else frame
180
+ )
181
+ if reference is None:
182
+ reference = canonical
183
+ else:
184
+ pd.testing.assert_frame_equal(reference, canonical)
185
+ assert not list(Path(directory).iterdir()), "Cache was written"
186
+ run_report = {
187
+ "round": round_number + 1,
188
+ "mode": mode,
189
+ "rows": len(frame),
190
+ "seconds": seconds,
191
+ "query_count": len(records),
192
+ "physical_input_bytes": sum(
193
+ record["physicalInputBytes"] for record in records
194
+ ),
195
+ "cpu_ms": sum(
196
+ record["cpuTimeMillis"] for record in records
197
+ ),
198
+ "queries": records,
199
+ }
200
+ report["runs"].append(run_report)
201
+ args.output.write_text(json.dumps(report, indent=2))
202
+ print(
203
+ json.dumps(
204
+ {k: v for k, v in run_report.items() if k != "queries"}
205
+ ),
206
+ flush=True,
207
+ )
208
+ report["identical_results"] = True
209
+ report["result_cache_files_written"] = 0
210
+ args.output.write_text(json.dumps(report, indent=2))
211
+
212
+
213
+ if __name__ == "__main__":
214
+ main()
@@ -26,6 +26,7 @@ class OpenSkyDBAPI(Protocol):
26
26
  departure_airport: str | list[str] | None = None,
27
27
  arrival_airport: str | list[str] | None = None,
28
28
  airport: str | list[str] | None = None,
29
+ routes: list[tuple[str, str]] | None = None,
29
30
  callsign: str | list[str] | None = None,
30
31
  icao24: str | list[str] | None = None,
31
32
  cached: bool = True,
@@ -39,16 +40,16 @@ class OpenSkyDBAPI(Protocol):
39
40
  start: timelike,
40
41
  stop: timelike | None = None,
41
42
  *args: Any,
42
- # date_delta: timedelta = timedelta(hours=1),
43
43
  callsign: str | list[str] | None = None,
44
44
  icao24: str | list[str] | None = None,
45
45
  serials: int | Iterable[int] | None = None,
46
46
  bounds: (
47
47
  str | HasBounds | tuple[float, float, float, float] | None
48
48
  ) = None,
49
- departure_airport: str | None = None,
50
- arrival_airport: str | None = None,
51
- airport: str | None = None,
49
+ departure_airport: str | list[str] | None = None,
50
+ arrival_airport: str | list[str] | None = None,
51
+ airport: str | list[str] | None = None,
52
+ routes: list[tuple[str, str]] | None = None,
52
53
  time_buffer: str | pd.Timedelta | None = None,
53
54
  cached: bool = True,
54
55
  compress: bool = False,
@@ -65,9 +66,10 @@ class OpenSkyDBAPI(Protocol):
65
66
  serials: int | Iterable[int] | None = None,
66
67
  bounds: HasBounds | tuple[float, float, float, float] | None = None,
67
68
  callsign: str | list[str] | None = None,
68
- departure_airport: str | None = None,
69
- arrival_airport: str | None = None,
70
- airport: str | None = None,
69
+ departure_airport: str | list[str] | None = None,
70
+ arrival_airport: str | list[str] | None = None,
71
+ airport: str | list[str] | None = None,
72
+ routes: list[tuple[str, str]] | None = None,
71
73
  cached: bool = True,
72
74
  compress: bool = False,
73
75
  limit: int | None = None,