From cd72b7659e1be58e2e4208e378874e22d31b0641 Mon Sep 17 00:00:00 2001 From: Damian Szumski Date: Wed, 15 Apr 2026 13:48:14 +0200 Subject: [PATCH] flight categories --- examples/basic_usage.py | 2 ++ src/fr24sdk/models/__init__.py | 3 +- src/fr24sdk/models/flight_category.py | 42 ++++++++++++++++++++++--- src/fr24sdk/resources/flight_summary.py | 15 ++++++++- tests/unit/test_flight_summary.py | 28 ++++++++++++++++- 5 files changed, 82 insertions(+), 8 deletions(-) diff --git a/examples/basic_usage.py b/examples/basic_usage.py index 1284020..51a2cca 100644 --- a/examples/basic_usage.py +++ b/examples/basic_usage.py @@ -13,6 +13,7 @@ from datetime import datetime, timezone from fr24sdk.client import Client +from fr24sdk.models.flight_category import FlightCategory from fr24sdk.models.geographic import AltitudeRange, Boundary # Configure basic logging to see SDK informational messages @@ -31,6 +32,7 @@ def main() -> None: flight_summary = client.flight_summary.get_light(flights=["KL1316"], flight_datetime_from=datetime(2025, 5, 13, 12, 0, 0), flight_datetime_to=datetime(2025, 5, 14, 17, 10, 0)) if flight_summary.data: print(flight_summary.data[0].fr24_id) + print(client.flight_summary.get_full(operating_as=["FDX"],flight_datetime_from=datetime(2026, 1, 15, 15, 0, 0),flight_datetime_to=datetime(2026, 1, 15, 15, 5, 0),categories=[FlightCategory.CARGO])) print(client.live.flight_positions.get_light(flights=["SK2752"])) print(client.live.flight_positions.get_light(bounds=Boundary(north=55.6, south=55.5, west=12.5, east=12.6))) print(client.live.flight_positions.get_full(bounds="55.6,55.5,12.5,12.6")) diff --git a/src/fr24sdk/models/__init__.py b/src/fr24sdk/models/__init__.py index cf0def1..4ef47fe 100644 --- a/src/fr24sdk/models/__init__.py +++ b/src/fr24sdk/models/__init__.py @@ -3,7 +3,7 @@ # SPDX-License-Identifier: MIT """Exposes data models for the Flightradar24 SDK.""" -from .flight_category import FlightCategory +from .flight_category import FlightCategory, FlightCategoryCode from .airline import AirlineLight from .airport import AirportFull, AirportLight, Country, Timezone from .flight import ( @@ -29,6 +29,7 @@ __all__ = [ "AirlineLight", "FlightCategory", + "FlightCategoryCode", "AirportFull", "AirportLight", "Country", diff --git a/src/fr24sdk/models/flight_category.py b/src/fr24sdk/models/flight_category.py index 0879d60..dff9f17 100644 --- a/src/fr24sdk/models/flight_category.py +++ b/src/fr24sdk/models/flight_category.py @@ -4,15 +4,44 @@ """Flight category enumeration for the Flightradar24 SDK.""" from enum import Enum +from typing import Literal + +FlightCategoryCode = Literal[ + "P", + "C", + "M", + "J", + "T", + "H", + "B", + "G", + "D", + "V", + "O", + "N", +] class FlightCategory(str, Enum): - """Enumeration of FlightRadar24 flight categories. - - Maps character literals used by the FR24 API to identify different - types of aircraft and flight operations. + """Aircraft / vehicle category codes used in API (e.g. flight summary). + + - **P** — **PASSENGER** — Commercial aircraft that carry passengers as their primary purpose. + - **C** — **CARGO** — Aircraft that carry only cargo. + - **M** — **MILITARY_AND_GOVERNMENT** — Aircraft operated by military or a governmental agency. + - **J** — **BUSINESS_JETS** — Larger private aircraft, such as Gulfstream, Bombardier, and Pilatus. + - **T** — **GENERAL_AVIATION** — Non-commercial transport flights, including private, ambulance, + aerial survey, flight training and instrument calibration aircraft. + - **H** — **HELICOPTERS** — Rotary wing aircraft. + - **B** — **LIGHTER_THAN_AIR** — Lighter-than-air aircraft include gas-filled airships of all kinds. + - **G** — **GLIDERS** — Unpowered aircraft. + - **D** — **DRONES** — Uncrewed aircraft, ranging from small consumer drones to larger UAVs. + - **V** — **GROUND_VEHICLES** — Transponder equipped vehicles, such as push-back tugs, fire trucks, + and operations vehicles. + - **O** — **OTHER** — Aircraft appearing on Flightradar24 not classified elsewhere + (International Space Station, UFOs, Santa, etc). + - **N** — **NON_CATEGORIZED** — Aircraft not yet placed into a category in the Flightradar24 database. """ - + PASSENGER = "P" CARGO = "C" MILITARY_AND_GOVERNMENT = "M" @@ -28,3 +57,6 @@ class FlightCategory(str, Enum): def __str__(self) -> str: return self.value + + +__all__ = ["FlightCategory", "FlightCategoryCode"] diff --git a/src/fr24sdk/resources/flight_summary.py b/src/fr24sdk/resources/flight_summary.py index 30715c7..bbfb96a 100644 --- a/src/fr24sdk/resources/flight_summary.py +++ b/src/fr24sdk/resources/flight_summary.py @@ -4,11 +4,12 @@ """Resource class for flight summary data.""" import warnings -from typing import Optional, Any, Annotated +from typing import Optional, Any, Annotated, Union from datetime import datetime from pydantic import BaseModel, model_serializer, StringConstraints, Field from ..transport import HttpTransport +from ..models.flight_category import FlightCategory from ..models.flight import ( FlightSummaryLightResponse, FlightSummaryFullResponse, @@ -21,6 +22,7 @@ AIRLINE_ICAO_PATTERN, AIRPORT_PARAM_PATTERN, ROUTE_PATTERN, + SERVICE_TYPES_PATTERN, SORT_PATTERN, ) @@ -53,6 +55,9 @@ class _FlightSummaryParams(BaseModel): Field(default=None, max_length=15) ) aircraft: Optional[list[str]] = Field(default=None, max_length=15) + categories: Optional[ + list[Union[FlightCategory, Annotated[str, StringConstraints(pattern=SERVICE_TYPES_PATTERN)]]] + ] = Field(default=None, max_length=15) sort: Optional[Annotated[str, StringConstraints(pattern=SORT_PATTERN)]] = None limit: Optional[Annotated[int, Field(ge=1, le=20000)]] = None @@ -97,6 +102,7 @@ def get_light( airports: Optional[list[str]] = None, routes: Optional[list[str]] = None, aircraft: Optional[list[str]] = None, + categories: Optional[list[str]] = None, sort: Optional[str] = None, limit: Optional[int] = None, ) -> FlightSummaryLightResponse: @@ -118,6 +124,7 @@ def get_light( airports=airports, routes=routes, aircraft=aircraft, + categories=categories, sort=sort, limit=limit, ).model_dump(exclude_none=True) @@ -140,6 +147,7 @@ def get_full( airports: Optional[list[str]] = None, routes: Optional[list[str]] = None, aircraft: Optional[list[str]] = None, + categories: Optional[list[str]] = None, sort: Optional[str] = None, limit: Optional[int] = None, ) -> FlightSummaryFullResponse: @@ -159,6 +167,7 @@ def get_full( airports=airports, routes=routes, aircraft=aircraft, + categories=categories, sort=sort, limit=limit, ).model_dump(exclude_none=True) @@ -181,6 +190,7 @@ def get_count( airports: Optional[list[str]] = None, routes: Optional[list[str]] = None, aircraft: Optional[list[str]] = None, + categories: Optional[list[str]] = None, ) -> CountResponse: """Return the number of flight-summary records that match the filters. @@ -199,6 +209,7 @@ def get_count( airports=airports, routes=routes, aircraft=aircraft, + categories=categories, ).model_dump(exclude_none=True) response = self._transport.request( "GET", f"{self.BASE_PATH}/count", params=params @@ -219,6 +230,7 @@ def count( airports: Optional[list[str]] = None, routes: Optional[list[str]] = None, aircraft: Optional[list[str]] = None, + categories: Optional[list[str]] = None, ) -> CountResponse: """Deprecated alias for :meth:`get_count`.""" warnings.warn( @@ -239,4 +251,5 @@ def count( airports=airports, routes=routes, aircraft=aircraft, + categories=categories, ) diff --git a/tests/unit/test_flight_summary.py b/tests/unit/test_flight_summary.py index 2279cfd..74f6909 100644 --- a/tests/unit/test_flight_summary.py +++ b/tests/unit/test_flight_summary.py @@ -9,13 +9,31 @@ from fr24sdk.resources.flight_summary import FlightSummaryResource, _FlightSummaryParams from fr24sdk.models.flight import ( + FlightSummaryFull, FlightSummaryLightResponse, FlightSummaryFullResponse, CountResponse, ) +from fr24sdk.models.flight_category import FlightCategory from fr24sdk.transport import HttpTransport +class TestFlightSummaryFullCategory: + """Parsing of ``category`` on :class:`FlightSummaryFull`.""" + + def test_known_code_coerces_to_enum(self) -> None: + row = FlightSummaryFull.model_validate({"fr24_id": "abc", "category": "M"}) + assert row.category == FlightCategory.MILITARY_AND_GOVERNMENT + + def test_unknown_code_left_as_str(self) -> None: + row = FlightSummaryFull.model_validate({"fr24_id": "abc", "category": "X"}) + assert row.category == "X" + + def test_category_omitted_is_none(self) -> None: + row = FlightSummaryFull.model_validate({"fr24_id": "abc"}) + assert row.category is None + + class TestFlightSummaryParams: """Test the _FlightSummaryParams class.""" @@ -40,6 +58,13 @@ def test_serialize_params(self): assert serialized["callsigns"] == "BAW123" assert serialized["aircraft"] == "B738,A320" + params_with_cat = _FlightSummaryParams( + flight_datetime_from=test_datetime, + flight_datetime_to=test_datetime, + categories=[FlightCategory.PASSENGER, "C"], + ) + assert params_with_cat._to_query_dict()["categories"] == "P,C" + # Check that None values are not included assert "registrations" not in serialized @@ -162,6 +187,7 @@ def test_get_full_success(self, flight_summary, mock_transport): "flight_time": 3600, "actual_distance": 250.5, "circle_distance": 230.0, + "category": "P", "hex": "40123A", "first_seen": "2023-01-01T09:45:00Z", "last_seen": "2023-01-01T11:15:00Z", @@ -179,13 +205,13 @@ def test_get_full_success(self, flight_summary, mock_transport): result = flight_summary.get_full(flight_ids=["35f2ffd9"]) # Verify results - print(result) assert isinstance(result, FlightSummaryFullResponse) assert len(result.data) == 1 assert result.data[0].fr24_id == "35f2ffd9" assert result.data[0].flight == "BA1234" assert result.data[0].runway_takeoff == "27L" assert result.data[0].flight_time == 3600 + assert result.data[0].category == FlightCategory.PASSENGER # Verify mock was called correctly mock_transport.request.assert_called_once()