FlightRadarAPI 1.4.2__tar.gz → 1.5.0__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.
- flightradarapi-1.5.0/.gitignore +31 -0
- flightradarapi-1.5.0/FlightRadar24/__init__.py +36 -0
- flightradarapi-1.5.0/FlightRadar24/api.py +522 -0
- {flightradarapi-1.4.2 → flightradarapi-1.5.0}/FlightRadar24/core.py +17 -8
- flightradarapi-1.5.0/FlightRadar24/entities/airport.py +193 -0
- flightradarapi-1.5.0/FlightRadar24/entities/entity.py +45 -0
- flightradarapi-1.5.0/FlightRadar24/entities/flight.py +240 -0
- flightradarapi-1.5.0/FlightRadar24/errors.py +20 -0
- flightradarapi-1.5.0/FlightRadar24/flight_tracker_config.py +23 -0
- flightradarapi-1.5.0/FlightRadar24/parsers.py +143 -0
- flightradarapi-1.5.0/FlightRadar24/py.typed +0 -0
- flightradarapi-1.5.0/FlightRadar24/request.py +271 -0
- {flightradarapi-1.4.2 → flightradarapi-1.5.0}/Makefile +5 -5
- {flightradarapi-1.4.2 → flightradarapi-1.5.0}/PKG-INFO +3 -2
- {flightradarapi-1.4.2 → flightradarapi-1.5.0}/pyproject.toml +21 -3
- flightradarapi-1.4.2/.flake8 +0 -11
- flightradarapi-1.4.2/.gitignore +0 -9
- flightradarapi-1.4.2/FlightRadar24/__init__.py +0 -18
- flightradarapi-1.4.2/FlightRadar24/api.py +0 -639
- flightradarapi-1.4.2/FlightRadar24/entities/airport.py +0 -178
- flightradarapi-1.4.2/FlightRadar24/entities/entity.py +0 -31
- flightradarapi-1.4.2/FlightRadar24/entities/flight.py +0 -208
- flightradarapi-1.4.2/FlightRadar24/errors.py +0 -17
- flightradarapi-1.4.2/FlightRadar24/request.py +0 -114
- flightradarapi-1.4.2/pytest.ini +0 -15
- flightradarapi-1.4.2/requirements.txt +0 -4
- flightradarapi-1.4.2/tests/conftest.py +0 -5
- flightradarapi-1.4.2/tests/package.py +0 -5
- flightradarapi-1.4.2/tests/test_api.py +0 -143
- flightradarapi-1.4.2/tests/util.py +0 -41
- {flightradarapi-1.4.2 → flightradarapi-1.5.0}/.tool-versions +0 -0
- {flightradarapi-1.4.2 → flightradarapi-1.5.0}/FlightRadar24/entities/__init__.py +0 -0
- {flightradarapi-1.4.2 → flightradarapi-1.5.0}/FlightRadar24/zones.py +0 -0
- {flightradarapi-1.4.2 → flightradarapi-1.5.0}/LICENSE +0 -0
- {flightradarapi-1.4.2 → flightradarapi-1.5.0}/README.md +0 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__
|
|
3
|
+
*.pyc
|
|
4
|
+
*.pyo
|
|
5
|
+
*.pyd
|
|
6
|
+
*.so
|
|
7
|
+
dist/
|
|
8
|
+
build/
|
|
9
|
+
*.egg-info/
|
|
10
|
+
.eggs/
|
|
11
|
+
.venv/
|
|
12
|
+
venv/
|
|
13
|
+
.mypy_cache/
|
|
14
|
+
.pytest_cache/
|
|
15
|
+
htmlcov/
|
|
16
|
+
.coverage
|
|
17
|
+
coverage.xml
|
|
18
|
+
|
|
19
|
+
# Node.js
|
|
20
|
+
node_modules/
|
|
21
|
+
|
|
22
|
+
# Editors / OS
|
|
23
|
+
.idea/
|
|
24
|
+
.vscode/
|
|
25
|
+
.DS_Store
|
|
26
|
+
Thumbs.db
|
|
27
|
+
|
|
28
|
+
# Misc
|
|
29
|
+
.env
|
|
30
|
+
.cache
|
|
31
|
+
*.log
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
Unofficial SDK for FlightRadar24.
|
|
5
|
+
|
|
6
|
+
This SDK provides flight and airport data available to the public
|
|
7
|
+
on the FlightRadar24 website.
|
|
8
|
+
|
|
9
|
+
See more information at:
|
|
10
|
+
https://www.flightradar24.com/premium/
|
|
11
|
+
https://www.flightradar24.com/terms-and-conditions
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
__author__ = "Jean Loui Bernard Silva de Jesus"
|
|
15
|
+
__version__ = "1.5.0"
|
|
16
|
+
|
|
17
|
+
from .api import FlightRadar24API
|
|
18
|
+
from .core import Countries
|
|
19
|
+
from .entities import Airport, Entity, Flight
|
|
20
|
+
from .errors import AirportNotFoundError, CloudflareError, FlightRadarError, LoginError
|
|
21
|
+
from .flight_tracker_config import FlightTrackerConfig
|
|
22
|
+
from .request import RetryPolicy
|
|
23
|
+
|
|
24
|
+
__all__ = [
|
|
25
|
+
"FlightRadar24API",
|
|
26
|
+
"Countries",
|
|
27
|
+
"Airport",
|
|
28
|
+
"Entity",
|
|
29
|
+
"Flight",
|
|
30
|
+
"AirportNotFoundError",
|
|
31
|
+
"CloudflareError",
|
|
32
|
+
"FlightRadarError",
|
|
33
|
+
"LoginError",
|
|
34
|
+
"FlightTrackerConfig",
|
|
35
|
+
"RetryPolicy",
|
|
36
|
+
]
|
|
@@ -0,0 +1,522 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
import dataclasses
|
|
4
|
+
import math
|
|
5
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
6
|
+
from typing import Any, Dict, List, Optional, Tuple, Union
|
|
7
|
+
from urllib.parse import quote
|
|
8
|
+
|
|
9
|
+
from .core import Core, Countries
|
|
10
|
+
from .entities.airport import Airport
|
|
11
|
+
from .entities.flight import Flight
|
|
12
|
+
from .errors import AirportNotFoundError, LoginError
|
|
13
|
+
from .flight_tracker_config import FlightTrackerConfig
|
|
14
|
+
from .parsers import parse_airlines_html, parse_airports_html
|
|
15
|
+
from .request import APIClient, RetryPolicy
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class FlightRadar24API:
|
|
19
|
+
"""
|
|
20
|
+
Main class of the FlightRadarAPI
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
def __init__(
|
|
24
|
+
self,
|
|
25
|
+
user: Optional[str] = None,
|
|
26
|
+
password: Optional[str] = None,
|
|
27
|
+
timeout: int = 30,
|
|
28
|
+
max_workers: int = 8,
|
|
29
|
+
impersonate: Optional[str] = None,
|
|
30
|
+
retry: Optional[RetryPolicy] = None,
|
|
31
|
+
):
|
|
32
|
+
"""
|
|
33
|
+
Constructor of the FlightRadar24API class.
|
|
34
|
+
|
|
35
|
+
:param user: Your email
|
|
36
|
+
:param password: Your password
|
|
37
|
+
:param timeout: Request timeout in seconds
|
|
38
|
+
:param max_workers: Maximum threads used when fetching flight details concurrently
|
|
39
|
+
:param impersonate: TLS impersonation profile (curl_cffi). Override when FR24
|
|
40
|
+
updates its Cloudflare bot mitigation faster than this library releases.
|
|
41
|
+
See ``FlightRadar24.request.DEFAULT_IMPERSONATE`` for the current default.
|
|
42
|
+
:param retry: Optional :class:`RetryPolicy` applied to transient failures
|
|
43
|
+
(``CloudflareError`` and curl_cffi network errors). Defaults to no retry.
|
|
44
|
+
"""
|
|
45
|
+
self.__flight_tracker_config = FlightTrackerConfig()
|
|
46
|
+
self.__login_data: Optional[Dict] = None
|
|
47
|
+
client_kwargs: Dict[str, Any] = {"retry": retry}
|
|
48
|
+
if impersonate:
|
|
49
|
+
client_kwargs["impersonate"] = impersonate
|
|
50
|
+
self.__client = APIClient(**client_kwargs)
|
|
51
|
+
|
|
52
|
+
self.timeout: int = timeout
|
|
53
|
+
self.max_workers: int = max_workers
|
|
54
|
+
|
|
55
|
+
if user is not None and password is not None:
|
|
56
|
+
self.login(user, password)
|
|
57
|
+
|
|
58
|
+
def get_airlines(self) -> List[Dict]:
|
|
59
|
+
"""
|
|
60
|
+
Return a list with all airlines.
|
|
61
|
+
"""
|
|
62
|
+
response = self.__client.request(Core.airlines_data_url, headers=Core.html_headers, timeout=self.timeout)
|
|
63
|
+
return parse_airlines_html(response.get_bytes_content())
|
|
64
|
+
|
|
65
|
+
def get_airline_logo(self, iata: str, icao: str) -> Optional[Tuple[bytes, str]]:
|
|
66
|
+
"""
|
|
67
|
+
Download the logo of an airline from FlightRadar24 and return it as bytes.
|
|
68
|
+
"""
|
|
69
|
+
iata, icao = iata.upper(), icao.upper()
|
|
70
|
+
|
|
71
|
+
first_logo_url = Core.airline_logo_url.format(iata, icao)
|
|
72
|
+
|
|
73
|
+
# Try to get the image by the first URL option.
|
|
74
|
+
response = self.__client.request(
|
|
75
|
+
first_logo_url, headers=Core.image_headers,
|
|
76
|
+
allowed_error_codes=[403, 404], timeout=self.timeout,
|
|
77
|
+
)
|
|
78
|
+
status_code = response.get_status_code()
|
|
79
|
+
|
|
80
|
+
if not (400 <= status_code < 500):
|
|
81
|
+
return response.get_bytes_content(), first_logo_url.split(".")[-1]
|
|
82
|
+
|
|
83
|
+
# Get the image by the second airline logo URL.
|
|
84
|
+
second_logo_url = Core.alternative_airline_logo_url.format(icao)
|
|
85
|
+
|
|
86
|
+
response = self.__client.request(
|
|
87
|
+
second_logo_url, headers=Core.image_headers,
|
|
88
|
+
allowed_error_codes=[403, 404], timeout=self.timeout,
|
|
89
|
+
)
|
|
90
|
+
status_code = response.get_status_code()
|
|
91
|
+
|
|
92
|
+
if not (400 <= status_code < 500):
|
|
93
|
+
return response.get_bytes_content(), second_logo_url.split(".")[-1]
|
|
94
|
+
|
|
95
|
+
return None
|
|
96
|
+
|
|
97
|
+
def get_airport(self, code: str, *, details: bool = False) -> Airport:
|
|
98
|
+
"""
|
|
99
|
+
Return basic information about a specific airport.
|
|
100
|
+
|
|
101
|
+
:param code: ICAO or IATA of the airport
|
|
102
|
+
:param details: If True, it returns an Airport instance with detailed information.
|
|
103
|
+
"""
|
|
104
|
+
if not (3 <= len(code) <= 4):
|
|
105
|
+
raise ValueError(f"The code '{code}' is invalid. It must be the IATA or ICAO of the airport.")
|
|
106
|
+
|
|
107
|
+
if details:
|
|
108
|
+
airport = Airport()
|
|
109
|
+
airport.set_airport_details(self.get_airport_details(code))
|
|
110
|
+
return airport
|
|
111
|
+
|
|
112
|
+
response = self.__client.request(
|
|
113
|
+
Core.airport_data_url.format(code),
|
|
114
|
+
headers=Core.json_headers, timeout=self.timeout,
|
|
115
|
+
)
|
|
116
|
+
content = response.get_json_content()
|
|
117
|
+
|
|
118
|
+
if not content or not content.get("details"):
|
|
119
|
+
raise AirportNotFoundError(f"Could not find an airport by the code '{code}'.")
|
|
120
|
+
|
|
121
|
+
return Airport(info=content["details"])
|
|
122
|
+
|
|
123
|
+
def get_airport_details(self, code: str, flight_limit: int = 100, page: int = 1) -> Dict:
|
|
124
|
+
"""
|
|
125
|
+
Return the airport details from FlightRadar24.
|
|
126
|
+
|
|
127
|
+
:param code: ICAO or IATA of the airport
|
|
128
|
+
:param flight_limit: Limit of flights related to the airport
|
|
129
|
+
:param page: Page of result to display
|
|
130
|
+
"""
|
|
131
|
+
if not (3 <= len(code) <= 4):
|
|
132
|
+
raise ValueError(f"The code '{code}' is invalid. It must be the IATA or ICAO of the airport.")
|
|
133
|
+
|
|
134
|
+
request_params: Dict[str, Any] = {"format": "json"}
|
|
135
|
+
|
|
136
|
+
if self.is_logged_in():
|
|
137
|
+
request_params["token"] = self.__client.get_cookie("_frPl")
|
|
138
|
+
|
|
139
|
+
# Insert the method parameters into the dictionary for the request.
|
|
140
|
+
request_params["code"] = code
|
|
141
|
+
request_params["limit"] = flight_limit
|
|
142
|
+
request_params["page"] = page
|
|
143
|
+
|
|
144
|
+
# Request details from the FlightRadar24.
|
|
145
|
+
response = self.__client.request(
|
|
146
|
+
Core.api_airport_data_url,
|
|
147
|
+
params=request_params,
|
|
148
|
+
headers=Core.json_headers,
|
|
149
|
+
allowed_error_codes=[400],
|
|
150
|
+
timeout=self.timeout,
|
|
151
|
+
)
|
|
152
|
+
content = response.get_json_content()
|
|
153
|
+
|
|
154
|
+
if response.get_status_code() == 400 and content.get("errors"):
|
|
155
|
+
errors = content["errors"]["errors"]["parameters"]
|
|
156
|
+
|
|
157
|
+
if errors.get("limit"):
|
|
158
|
+
raise ValueError(errors["limit"]["notBetween"])
|
|
159
|
+
|
|
160
|
+
raise AirportNotFoundError(f"Could not find an airport by the code '{code}'.", errors)
|
|
161
|
+
|
|
162
|
+
result = content["result"]["response"]
|
|
163
|
+
|
|
164
|
+
# Check whether it received data of an airport.
|
|
165
|
+
data = result.get("airport", dict()).get("pluginData", dict())
|
|
166
|
+
|
|
167
|
+
if "details" not in data and len(data.get("runways", [])) == 0 and len(data) <= 3:
|
|
168
|
+
raise AirportNotFoundError(f"Could not find an airport by the code '{code}'.")
|
|
169
|
+
|
|
170
|
+
# Return the airport details.
|
|
171
|
+
return result
|
|
172
|
+
|
|
173
|
+
def get_airport_disruptions(self) -> Dict:
|
|
174
|
+
"""
|
|
175
|
+
Return airport disruptions.
|
|
176
|
+
"""
|
|
177
|
+
response = self.__client.request(
|
|
178
|
+
Core.airport_disruptions_url,
|
|
179
|
+
headers=Core.json_headers, timeout=self.timeout,
|
|
180
|
+
)
|
|
181
|
+
return response.get_json_content()
|
|
182
|
+
|
|
183
|
+
def get_airports(self, countries: List[Countries]) -> List[Airport]:
|
|
184
|
+
"""
|
|
185
|
+
Return a list with all airports for specified countries.
|
|
186
|
+
|
|
187
|
+
:param countries: List of country names from Countries enum.
|
|
188
|
+
"""
|
|
189
|
+
def _fetch(country):
|
|
190
|
+
href = Core.airports_data_url + "/" + country.value
|
|
191
|
+
response = self.__client.request_standalone(href, headers=Core.html_headers, timeout=self.timeout)
|
|
192
|
+
return parse_airports_html(response.get_bytes_content(), href)
|
|
193
|
+
|
|
194
|
+
with ThreadPoolExecutor(max_workers=self.max_workers) as executor:
|
|
195
|
+
results = executor.map(_fetch, countries)
|
|
196
|
+
|
|
197
|
+
return [airport for result in results for airport in result]
|
|
198
|
+
|
|
199
|
+
def get_bookmarks(self) -> Dict:
|
|
200
|
+
"""
|
|
201
|
+
Get the bookmarks from the FlightRadar24 account.
|
|
202
|
+
"""
|
|
203
|
+
if self.__login_data is None:
|
|
204
|
+
raise LoginError("You must log in to your account.")
|
|
205
|
+
|
|
206
|
+
headers = {**Core.json_headers, "accesstoken": self.get_login_data()["accessToken"]}
|
|
207
|
+
|
|
208
|
+
response = self.__client.request(Core.bookmarks_url, headers=headers, timeout=self.timeout)
|
|
209
|
+
return response.get_json_content()
|
|
210
|
+
|
|
211
|
+
def get_bounds(self, zone: Dict[str, float]) -> str:
|
|
212
|
+
"""
|
|
213
|
+
Convert coordinate dictionary to a string "y1, y2, x1, x2".
|
|
214
|
+
|
|
215
|
+
:param zone: Dictionary containing the following keys: tl_y, tl_x, br_y, br_x
|
|
216
|
+
"""
|
|
217
|
+
return f"{zone['tl_y']},{zone['br_y']},{zone['tl_x']},{zone['br_x']}"
|
|
218
|
+
|
|
219
|
+
def get_bounds_by_point(self, latitude: float, longitude: float, radius: float) -> str:
|
|
220
|
+
"""
|
|
221
|
+
Convert a point coordinate and a radius to a string "y1, y2, x1, x2".
|
|
222
|
+
|
|
223
|
+
:param latitude: Latitude of the point
|
|
224
|
+
:param longitude: Longitude of the point
|
|
225
|
+
:param radius: Radius in meters to create area around the point
|
|
226
|
+
"""
|
|
227
|
+
half_side_in_km = abs(radius) / 1000
|
|
228
|
+
|
|
229
|
+
lat = math.radians(latitude)
|
|
230
|
+
lon = math.radians(longitude)
|
|
231
|
+
|
|
232
|
+
approx_earth_radius = 6371
|
|
233
|
+
hypotenuse_distance = math.sqrt(2 * (math.pow(half_side_in_km, 2)))
|
|
234
|
+
|
|
235
|
+
lat_min = math.asin(
|
|
236
|
+
math.sin(lat) * math.cos(hypotenuse_distance / approx_earth_radius)
|
|
237
|
+
+ math.cos(lat)
|
|
238
|
+
* math.sin(hypotenuse_distance / approx_earth_radius)
|
|
239
|
+
* math.cos(225 * (math.pi / 180)),
|
|
240
|
+
)
|
|
241
|
+
lon_min = lon + math.atan2(
|
|
242
|
+
math.sin(225 * (math.pi / 180))
|
|
243
|
+
* math.sin(hypotenuse_distance / approx_earth_radius)
|
|
244
|
+
* math.cos(lat),
|
|
245
|
+
math.cos(hypotenuse_distance / approx_earth_radius)
|
|
246
|
+
- math.sin(lat) * math.sin(lat_min),
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
lat_max = math.asin(
|
|
250
|
+
math.sin(lat) * math.cos(hypotenuse_distance / approx_earth_radius)
|
|
251
|
+
+ math.cos(lat)
|
|
252
|
+
* math.sin(hypotenuse_distance / approx_earth_radius)
|
|
253
|
+
* math.cos(45 * (math.pi / 180)),
|
|
254
|
+
)
|
|
255
|
+
lon_max = lon + math.atan2(
|
|
256
|
+
math.sin(45 * (math.pi / 180))
|
|
257
|
+
* math.sin(hypotenuse_distance / approx_earth_radius)
|
|
258
|
+
* math.cos(lat),
|
|
259
|
+
math.cos(hypotenuse_distance / approx_earth_radius)
|
|
260
|
+
- math.sin(lat) * math.sin(lat_max),
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
rad2deg = math.degrees
|
|
264
|
+
|
|
265
|
+
zone = {
|
|
266
|
+
"tl_y": rad2deg(lat_max),
|
|
267
|
+
"br_y": rad2deg(lat_min),
|
|
268
|
+
"tl_x": rad2deg(lon_min),
|
|
269
|
+
"br_x": rad2deg(lon_max)
|
|
270
|
+
}
|
|
271
|
+
return self.get_bounds(zone)
|
|
272
|
+
|
|
273
|
+
def get_country_flag(self, country: str) -> Optional[Tuple[bytes, str]]:
|
|
274
|
+
"""
|
|
275
|
+
Download the flag of a country from FlightRadar24 and return it as bytes.
|
|
276
|
+
|
|
277
|
+
:param country: Country name
|
|
278
|
+
"""
|
|
279
|
+
flag_url = Core.country_flag_url.format(country.lower().replace(" ", "-"))
|
|
280
|
+
headers = Core.image_headers.copy()
|
|
281
|
+
|
|
282
|
+
headers.pop("origin", None) # Does not work for this request.
|
|
283
|
+
|
|
284
|
+
response = self.__client.request(
|
|
285
|
+
flag_url, headers=headers,
|
|
286
|
+
allowed_error_codes=[403, 404], timeout=self.timeout,
|
|
287
|
+
)
|
|
288
|
+
status_code = response.get_status_code()
|
|
289
|
+
|
|
290
|
+
if not (400 <= status_code < 500):
|
|
291
|
+
return response.get_bytes_content(), flag_url.split(".")[-1]
|
|
292
|
+
|
|
293
|
+
return None
|
|
294
|
+
|
|
295
|
+
def get_flight_details(self, flight: Flight) -> Dict[Any, Any]:
|
|
296
|
+
"""
|
|
297
|
+
Return the flight details from Data Live FlightRadar24.
|
|
298
|
+
|
|
299
|
+
:param flight: A Flight instance
|
|
300
|
+
"""
|
|
301
|
+
response = self.__client.request_standalone(
|
|
302
|
+
Core.flight_data_url.format(flight.id), headers=Core.json_headers, timeout=self.timeout,
|
|
303
|
+
)
|
|
304
|
+
return response.get_json_content()
|
|
305
|
+
|
|
306
|
+
def get_flights(
|
|
307
|
+
self,
|
|
308
|
+
airline: Optional[str] = None,
|
|
309
|
+
bounds: Optional[str] = None,
|
|
310
|
+
registration: Optional[str] = None,
|
|
311
|
+
aircraft_type: Optional[str] = None,
|
|
312
|
+
*,
|
|
313
|
+
details: bool = False
|
|
314
|
+
) -> List[Flight]:
|
|
315
|
+
"""
|
|
316
|
+
Return a list of flights. See more options at set_flight_tracker_config() method.
|
|
317
|
+
|
|
318
|
+
:param airline: The airline ICAO. Ex: "DAL"
|
|
319
|
+
:param bounds: Coordinates (y1, y2 ,x1, x2). Ex: "75.78,-75.78,-427.56,427.56"
|
|
320
|
+
:param registration: Aircraft registration
|
|
321
|
+
:param aircraft_type: Aircraft model code. Ex: "B737"
|
|
322
|
+
:param details: If True, it returns flights with detailed information
|
|
323
|
+
"""
|
|
324
|
+
request_params = dataclasses.asdict(self.__flight_tracker_config)
|
|
325
|
+
|
|
326
|
+
if self.is_logged_in():
|
|
327
|
+
request_params["enc"] = self.__client.get_cookie("_frPl")
|
|
328
|
+
|
|
329
|
+
# Insert the method parameters into the dictionary for the request.
|
|
330
|
+
if airline is not None: request_params["airline"] = airline
|
|
331
|
+
if bounds is not None: request_params["bounds"] = bounds
|
|
332
|
+
if registration is not None: request_params["reg"] = registration
|
|
333
|
+
if aircraft_type is not None: request_params["type"] = aircraft_type
|
|
334
|
+
|
|
335
|
+
# Get all flights from Data Live FlightRadar24.
|
|
336
|
+
response = self.__client.request(
|
|
337
|
+
Core.real_time_flight_tracker_data_url,
|
|
338
|
+
params=request_params,
|
|
339
|
+
headers=Core.json_headers,
|
|
340
|
+
timeout=self.timeout,
|
|
341
|
+
)
|
|
342
|
+
content = response.get_json_content()
|
|
343
|
+
|
|
344
|
+
flights: List[Flight] = list()
|
|
345
|
+
|
|
346
|
+
for flight_id, flight_info in content.items():
|
|
347
|
+
|
|
348
|
+
# Get flights only.
|
|
349
|
+
if not flight_id[0].isnumeric():
|
|
350
|
+
continue
|
|
351
|
+
|
|
352
|
+
flights.append(Flight(flight_id, flight_info))
|
|
353
|
+
|
|
354
|
+
if details:
|
|
355
|
+
with ThreadPoolExecutor(max_workers=self.max_workers) as executor:
|
|
356
|
+
futures = {executor.submit(self.get_flight_details, f): f for f in flights}
|
|
357
|
+
for future in as_completed(futures):
|
|
358
|
+
futures[future].set_flight_details(future.result())
|
|
359
|
+
|
|
360
|
+
return flights
|
|
361
|
+
|
|
362
|
+
def get_flight_tracker_config(self) -> FlightTrackerConfig:
|
|
363
|
+
"""
|
|
364
|
+
Return a copy of the current config of the Real Time Flight Tracker, used by get_flights() method.
|
|
365
|
+
"""
|
|
366
|
+
return dataclasses.replace(self.__flight_tracker_config)
|
|
367
|
+
|
|
368
|
+
def get_history_data(self, flight: Flight, file_type: str, timestamp: int) -> str:
|
|
369
|
+
"""
|
|
370
|
+
Download historical data of a flight.
|
|
371
|
+
|
|
372
|
+
:param flight: A Flight instance
|
|
373
|
+
:param file_type: Must be "CSV" or "KML"
|
|
374
|
+
:param timestamp: A Unix timestamp
|
|
375
|
+
"""
|
|
376
|
+
if self.__login_data is None:
|
|
377
|
+
raise LoginError("You must log in to your account.")
|
|
378
|
+
|
|
379
|
+
file_type = file_type.lower()
|
|
380
|
+
|
|
381
|
+
if file_type not in ["csv", "kml"]:
|
|
382
|
+
raise ValueError(f"File type '{file_type}' is not supported. Only CSV and KML are supported.")
|
|
383
|
+
|
|
384
|
+
headers = {**Core.json_headers, "accesstoken": self.get_login_data()["accessToken"]}
|
|
385
|
+
|
|
386
|
+
response = self.__client.request(
|
|
387
|
+
Core.historical_data_url.format(flight.id, file_type, timestamp),
|
|
388
|
+
headers=headers,
|
|
389
|
+
timeout=self.timeout,
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
return response.get_bytes_content().decode("utf-8")
|
|
393
|
+
|
|
394
|
+
def get_login_data(self) -> Dict[Any, Any]:
|
|
395
|
+
"""
|
|
396
|
+
Return the user data.
|
|
397
|
+
"""
|
|
398
|
+
if self.__login_data is None:
|
|
399
|
+
raise LoginError("You must log in to your account.")
|
|
400
|
+
|
|
401
|
+
return self.__login_data["userData"].copy()
|
|
402
|
+
|
|
403
|
+
def get_most_tracked(self) -> Dict:
|
|
404
|
+
"""
|
|
405
|
+
Return the most tracked data.
|
|
406
|
+
"""
|
|
407
|
+
response = self.__client.request(Core.most_tracked_url, headers=Core.json_headers, timeout=self.timeout)
|
|
408
|
+
return response.get_json_content()
|
|
409
|
+
|
|
410
|
+
def get_volcanic_eruptions(self) -> Dict:
|
|
411
|
+
"""
|
|
412
|
+
Return boundaries of volcanic eruptions and ash clouds impacting aviation.
|
|
413
|
+
"""
|
|
414
|
+
response = self.__client.request(
|
|
415
|
+
Core.volcanic_eruption_data_url,
|
|
416
|
+
headers=Core.json_headers, timeout=self.timeout,
|
|
417
|
+
)
|
|
418
|
+
return response.get_json_content()
|
|
419
|
+
|
|
420
|
+
def get_zones(self) -> Dict[str, Any]:
|
|
421
|
+
"""
|
|
422
|
+
Return all major zones on the globe.
|
|
423
|
+
"""
|
|
424
|
+
zones = Core.static_zones.copy()
|
|
425
|
+
zones.pop("version", None)
|
|
426
|
+
return zones
|
|
427
|
+
|
|
428
|
+
def search(self, query: str, limit: int = 50) -> Dict:
|
|
429
|
+
"""
|
|
430
|
+
Return the search result.
|
|
431
|
+
"""
|
|
432
|
+
response = self.__client.request(
|
|
433
|
+
Core.search_data_url.format(quote(query), limit),
|
|
434
|
+
headers=Core.json_headers, timeout=self.timeout,
|
|
435
|
+
)
|
|
436
|
+
content = response.get_json_content()
|
|
437
|
+
results = content.get("results", [])
|
|
438
|
+
stats = content.get("stats", {})
|
|
439
|
+
|
|
440
|
+
i = 0
|
|
441
|
+
data: Dict[str, Any] = {}
|
|
442
|
+
for name, count in stats.get("count", {}).items():
|
|
443
|
+
data[name] = results[i:i + count]
|
|
444
|
+
i += count
|
|
445
|
+
return data
|
|
446
|
+
|
|
447
|
+
def is_logged_in(self) -> bool:
|
|
448
|
+
"""
|
|
449
|
+
Check if the user is logged into the FlightRadar24 account.
|
|
450
|
+
"""
|
|
451
|
+
return self.__login_data is not None
|
|
452
|
+
|
|
453
|
+
def login(self, user: str, password: str) -> None:
|
|
454
|
+
"""
|
|
455
|
+
Log in to a FlightRadar24 account.
|
|
456
|
+
|
|
457
|
+
:param user: Your email.
|
|
458
|
+
:param password: Your password.
|
|
459
|
+
"""
|
|
460
|
+
self.__login_data = None
|
|
461
|
+
self.__client.clear_cookies()
|
|
462
|
+
|
|
463
|
+
data = {
|
|
464
|
+
"email": user,
|
|
465
|
+
"password": password,
|
|
466
|
+
"remember": "true",
|
|
467
|
+
"type": "web"
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
response = self.__client.request(
|
|
471
|
+
Core.user_login_url,
|
|
472
|
+
headers=Core.json_headers, data=data, timeout=self.timeout,
|
|
473
|
+
)
|
|
474
|
+
status_code = response.get_status_code()
|
|
475
|
+
content = response.get_json_content()
|
|
476
|
+
|
|
477
|
+
if not (200 <= status_code < 300) or not content.get("success"):
|
|
478
|
+
raise LoginError(content.get("message", "Your email or password is incorrect"))
|
|
479
|
+
|
|
480
|
+
self.__login_data = {
|
|
481
|
+
"userData": content["userData"],
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
def logout(self) -> bool:
|
|
485
|
+
"""
|
|
486
|
+
Log out of the FlightRadar24 account.
|
|
487
|
+
|
|
488
|
+
Return a boolean indicating that it successfully logged out of the server.
|
|
489
|
+
"""
|
|
490
|
+
if self.__login_data is None:
|
|
491
|
+
return True
|
|
492
|
+
|
|
493
|
+
self.__login_data = None
|
|
494
|
+
try:
|
|
495
|
+
response = self.__client.request(Core.user_logout_url, headers=Core.json_headers, timeout=self.timeout)
|
|
496
|
+
return 200 <= response.get_status_code() < 300
|
|
497
|
+
finally:
|
|
498
|
+
self.__client.clear_cookies()
|
|
499
|
+
|
|
500
|
+
def set_flight_tracker_config(
|
|
501
|
+
self,
|
|
502
|
+
flight_tracker_config: Optional[FlightTrackerConfig] = None,
|
|
503
|
+
**config: Union[int, str]
|
|
504
|
+
) -> None:
|
|
505
|
+
"""
|
|
506
|
+
Set config for the Real Time Flight Tracker, used by get_flights() method.
|
|
507
|
+
"""
|
|
508
|
+
if flight_tracker_config is not None:
|
|
509
|
+
self.__flight_tracker_config = flight_tracker_config
|
|
510
|
+
|
|
511
|
+
current_config_dict = dataclasses.asdict(self.__flight_tracker_config)
|
|
512
|
+
|
|
513
|
+
for key, value in config.items():
|
|
514
|
+
value = str(value)
|
|
515
|
+
|
|
516
|
+
if key not in current_config_dict:
|
|
517
|
+
raise KeyError(f"Unknown option: '{key}'")
|
|
518
|
+
|
|
519
|
+
if not value.isdecimal():
|
|
520
|
+
raise TypeError(f"Value must be a number. Got '{value}' for key '{key}'")
|
|
521
|
+
|
|
522
|
+
setattr(self.__flight_tracker_config, key, value)
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
# -*- coding: utf-8 -*-
|
|
2
2
|
|
|
3
|
-
from abc import ABC
|
|
4
3
|
from enum import Enum
|
|
5
4
|
from .zones import static_zones
|
|
6
5
|
|
|
7
6
|
|
|
8
|
-
class Core
|
|
7
|
+
class Core:
|
|
9
8
|
|
|
10
9
|
# Base URLs.
|
|
11
10
|
api_flightradar_base_url = "https://api.flightradar24.com/common/v1"
|
|
@@ -62,7 +61,7 @@ class Core(ABC):
|
|
|
62
61
|
|
|
63
62
|
headers = {
|
|
64
63
|
"accept-encoding": "gzip, br",
|
|
65
|
-
"accept-language": "
|
|
64
|
+
"accept-language": "en-US,en;q=0.9",
|
|
66
65
|
"cache-control": "max-age=0",
|
|
67
66
|
"origin": "https://www.flightradar24.com",
|
|
68
67
|
"referer": "https://www.flightradar24.com/",
|
|
@@ -72,7 +71,10 @@ class Core(ABC):
|
|
|
72
71
|
"sec-fetch-dest": "empty",
|
|
73
72
|
"sec-fetch-mode": "cors",
|
|
74
73
|
"sec-fetch-site": "same-site",
|
|
75
|
-
"user-agent":
|
|
74
|
+
"user-agent": (
|
|
75
|
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
|
|
76
|
+
" (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36"
|
|
77
|
+
),
|
|
76
78
|
}
|
|
77
79
|
|
|
78
80
|
json_headers = headers.copy()
|
|
@@ -82,9 +84,13 @@ class Core(ABC):
|
|
|
82
84
|
image_headers["accept"] = "image/gif, image/jpg, image/jpeg, image/png"
|
|
83
85
|
|
|
84
86
|
html_headers = {
|
|
85
|
-
"accept":
|
|
87
|
+
"accept": (
|
|
88
|
+
"text/html,application/xhtml+xml,application/xml;q=0.9,"
|
|
89
|
+
"image/avif,image/webp,image/apng,*/*;q=0.8,"
|
|
90
|
+
"application/signed-exchange;v=b3;q=0.7"
|
|
91
|
+
),
|
|
86
92
|
"accept-encoding": "gzip, deflate, br",
|
|
87
|
-
"accept-language": "
|
|
93
|
+
"accept-language": "en-US,en;q=0.9",
|
|
88
94
|
"cache-control": "max-age=0",
|
|
89
95
|
"referer": "https://www.flightradar24.com/",
|
|
90
96
|
"sec-ch-ua": '"Google Chrome";v="136", "Chromium";v="136", "Not-A.Brand";v="24"',
|
|
@@ -95,7 +101,10 @@ class Core(ABC):
|
|
|
95
101
|
"sec-fetch-site": "same-origin",
|
|
96
102
|
"sec-fetch-user": "?1",
|
|
97
103
|
"upgrade-insecure-requests": "1",
|
|
98
|
-
"user-agent":
|
|
104
|
+
"user-agent": (
|
|
105
|
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"
|
|
106
|
+
" (KHTML, like Gecko) Chrome/136.0.0.0 Safari/537.36"
|
|
107
|
+
),
|
|
99
108
|
}
|
|
100
109
|
|
|
101
110
|
|
|
@@ -330,4 +339,4 @@ class Countries(Enum):
|
|
|
330
339
|
WALLIS_AND_FUTUNA = "wallis-and-futuna"
|
|
331
340
|
YEMEN = "yemen"
|
|
332
341
|
ZAMBIA = "zambia"
|
|
333
|
-
ZIMBABWE = "zimbabwe"
|
|
342
|
+
ZIMBABWE = "zimbabwe"
|