Initial Vibe coded project
This commit is contained in:
commit
0891cf12a5
21 files changed
+2830
No files matched your search
@@ -0,0 +1,116 @@
|
||||
import logging
|
||||
import os
|
||||
import sqlite3
|
||||
import time
|
||||
from urllib.parse import urlencode
|
||||
|
||||
from flight_map.geo import Bounds
|
||||
from flight_map.network import request_json
|
||||
from flight_map.storage import Observation, Session, checkpoint, save_observations
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
TOKEN_URL = (
|
||||
"https://auth.opensky-network.org/auth/realms/opensky-network/protocol/openid-connect/token"
|
||||
)
|
||||
|
||||
|
||||
class OpenSky:
|
||||
def __init__(self) -> None:
|
||||
self.client_id = os.environ.get("OPENSKY_CLIENT_ID")
|
||||
self.secret = os.environ.get("OPENSKY_CLIENT_SECRET")
|
||||
self.token = os.environ.get("OPENSKY_ACCESS_TOKEN")
|
||||
self.expires = float("inf") if self.token else 0.0
|
||||
if bool(self.client_id) != bool(self.secret):
|
||||
raise ValueError("Set both OPENSKY_CLIENT_ID and OPENSKY_CLIENT_SECRET.")
|
||||
if not self.client_id and not self.token:
|
||||
LOGGER.warning("Using anonymous OpenSky access; daily credit limits apply.")
|
||||
|
||||
def observations(self, bounds: Bounds) -> list[Observation]:
|
||||
if self.client_id and time.time() >= self.expires:
|
||||
response = request_json(
|
||||
TOKEN_URL,
|
||||
data=urlencode(
|
||||
{
|
||||
"grant_type": "client_credentials",
|
||||
"client_id": self.client_id,
|
||||
"client_secret": self.secret,
|
||||
}
|
||||
).encode(),
|
||||
headers={"Content-Type": "application/x-www-form-urlencoded"},
|
||||
)
|
||||
if not isinstance(response, dict) or not isinstance(response.get("access_token"), str):
|
||||
raise RuntimeError("OpenSky authentication returned no access token.")
|
||||
self.token = response["access_token"]
|
||||
self.expires = time.time() + float(response.get("expires_in", 300)) - 30
|
||||
headers = {"Authorization": f"Bearer {self.token}"} if self.token else {}
|
||||
query = urlencode(
|
||||
{
|
||||
"lomin": bounds.west,
|
||||
"lamin": bounds.south,
|
||||
"lomax": bounds.east,
|
||||
"lamax": bounds.north,
|
||||
}
|
||||
)
|
||||
response = request_json(
|
||||
"https://opensky-network.org/api/states/all?" + query, headers=headers
|
||||
)
|
||||
if not isinstance(response, dict) or "states" not in response:
|
||||
raise RuntimeError("OpenSky response is missing aircraft states.")
|
||||
states = response["states"]
|
||||
if states is None:
|
||||
return []
|
||||
if not isinstance(states, list):
|
||||
raise RuntimeError("OpenSky aircraft states are malformed.")
|
||||
observations = []
|
||||
for state in states:
|
||||
if not isinstance(state, list) or len(state) < 14:
|
||||
raise RuntimeError("OpenSky returned a malformed aircraft state.")
|
||||
if any(state[index] is None for index in (3, 5, 6)):
|
||||
continue
|
||||
try:
|
||||
observations.append(
|
||||
Observation(
|
||||
str(state[0]).lower(),
|
||||
str(state[1]).strip() or None if state[1] else None,
|
||||
float(state[3]),
|
||||
float(state[5]),
|
||||
float(state[6]),
|
||||
float(state[7]) if state[7] is not None else None,
|
||||
float(state[13]) if state[13] is not None else None,
|
||||
bool(state[8]),
|
||||
)
|
||||
)
|
||||
except (ValueError, TypeError) as exc:
|
||||
raise RuntimeError("OpenSky returned invalid aircraft observation values.") from exc
|
||||
return observations
|
||||
|
||||
|
||||
def collect(
|
||||
connection: sqlite3.Connection,
|
||||
session: Session,
|
||||
client: OpenSky,
|
||||
duration: float | None = None,
|
||||
) -> None:
|
||||
started = time.monotonic()
|
||||
checkpoint(connection, session.id, time.time(), "recording")
|
||||
try:
|
||||
while True:
|
||||
tick = time.monotonic()
|
||||
observations = client.observations(session.collection_bounds)
|
||||
save_observations(connection, session, observations, time.time())
|
||||
LOGGER.info("Session %s: received %d observations", session.id, len(observations))
|
||||
remaining = None if duration is None else duration - (time.monotonic() - started)
|
||||
if remaining is not None and remaining <= 0:
|
||||
break
|
||||
delay = max(0, session.interval - (time.monotonic() - tick))
|
||||
time.sleep(delay if remaining is None else min(delay, remaining))
|
||||
if duration is not None and time.monotonic() - started >= duration:
|
||||
break
|
||||
except KeyboardInterrupt:
|
||||
checkpoint(connection, session.id, time.time(), "interrupted")
|
||||
LOGGER.info("Recording interrupted; committed observations are preserved.")
|
||||
return
|
||||
except (RuntimeError, ValueError, OSError, sqlite3.Error):
|
||||
checkpoint(connection, session.id, time.time(), "failed")
|
||||
raise
|
||||
checkpoint(connection, session.id, time.time(), "stopped")
|
||||
Reference in new issue
Block a user