plane-above 0.1.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.
- plane_above-0.1.0/LICENSE +21 -0
- plane_above-0.1.0/PKG-INFO +214 -0
- plane_above-0.1.0/README.md +185 -0
- plane_above-0.1.0/plane_above/__init__.py +6 -0
- plane_above-0.1.0/plane_above/_plane_above.py +139 -0
- plane_above-0.1.0/plane_above/aircraft.py +195 -0
- plane_above-0.1.0/plane_above/osn.py +146 -0
- plane_above-0.1.0/plane_above/route.py +111 -0
- plane_above-0.1.0/plane_above/static/__init__.py +34 -0
- plane_above-0.1.0/plane_above/static/constants.py +2 -0
- plane_above-0.1.0/plane_above/static/sources.py +25 -0
- plane_above-0.1.0/plane_above/utils/__init__.py +4 -0
- plane_above-0.1.0/plane_above/utils/http.py +31 -0
- plane_above-0.1.0/plane_above/utils/logger.py +5 -0
- plane_above-0.1.0/plane_above.egg-info/PKG-INFO +214 -0
- plane_above-0.1.0/plane_above.egg-info/SOURCES.txt +26 -0
- plane_above-0.1.0/plane_above.egg-info/dependency_links.txt +1 -0
- plane_above-0.1.0/plane_above.egg-info/requires.txt +4 -0
- plane_above-0.1.0/plane_above.egg-info/top_level.txt +1 -0
- plane_above-0.1.0/pyproject.toml +119 -0
- plane_above-0.1.0/setup.cfg +4 -0
- plane_above-0.1.0/tests/test_fetch_plane.py +177 -0
- plane_above-0.1.0/tests/test_get_aircraft.py +122 -0
- plane_above-0.1.0/tests/test_get_photo.py +158 -0
- plane_above-0.1.0/tests/test_get_route.py +238 -0
- plane_above-0.1.0/tests/test_osn_bbox.py +73 -0
- plane_above-0.1.0/tests/test_osn_filter.py +77 -0
- plane_above-0.1.0/tests/test_osn_retrieve.py +61 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026-Present Ilia Andreevich
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: plane-above
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: What’s flying up there, huh?
|
|
5
|
+
Author: Ilia Andreevich
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/wrongcredentials/plane-above/
|
|
8
|
+
Project-URL: Repository, https://github.com/wrongcredentials/plane-above
|
|
9
|
+
Project-URL: Issues, https://github.com/wrongcredentials/plane-above/issues
|
|
10
|
+
Keywords: aviation,aircraft,planes,flight-tracking,air-traffic,opensky
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
20
|
+
Classifier: Topic :: Scientific/Engineering :: Information Analysis
|
|
21
|
+
Requires-Python: >=3.10
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
License-File: LICENSE
|
|
24
|
+
Requires-Dist: httpx>=0.28.0
|
|
25
|
+
Requires-Dist: lxml>=5.1.0
|
|
26
|
+
Requires-Dist: python-dateutil>=2.9.0
|
|
27
|
+
Requires-Dist: country-converter>=1.3.0
|
|
28
|
+
Dynamic: license-file
|
|
29
|
+
|
|
30
|
+
<img src="logo.svg" alt="plane-above" width="200">
|
|
31
|
+
|
|
32
|
+
# Plane Above
|
|
33
|
+
Plane Above is a python package featuring a convinient way to retrieve planes above certain point.
|
|
34
|
+
|
|
35
|
+
<details>
|
|
36
|
+
<summary>One notable usage example...</summary>
|
|
37
|
+
|
|
38
|
+
Try <a href="https://t.me/plane_above_bot">Plane Above</a> telegram bot.
|
|
39
|
+
<img src="tg.png" alt="plane-above-bot" width="400">
|
|
40
|
+
</details>
|
|
41
|
+
|
|
42
|
+
## Before we start...
|
|
43
|
+
To collect data, this library utilizes various sources. They are open and maintained by enthusiasts mostly.
|
|
44
|
+
So, use responsively! Don't push the limits and take actions with care.
|
|
45
|
+
Please mind that data might be missing, incorrect or outdated.
|
|
46
|
+
|
|
47
|
+
## How it works
|
|
48
|
+
_Note: here and after I would refer to any flying object as an «aircraft» or «plane»,
|
|
49
|
+
but keep in mind that it also includes helicopters, gliders, rotorcraft and even balloons._
|
|
50
|
+
#### 1. BUILD SEARCHING AREA
|
|
51
|
+
With given latitude and longitude of point it builds area of search by calculating bounding box coordinates
|
|
52
|
+
based on required distance from point.
|
|
53
|
+
#### 2. RETRIEVE STATES
|
|
54
|
+
Next it makes a request to OpenSky API to retrieve all states in searching area.
|
|
55
|
+
Each state represents [state vector](https://openskynetwork.github.io/opensky-api/index.html#state-vectors)
|
|
56
|
+
and contains information of ADS-B messages from transponder installed in aircraft.
|
|
57
|
+
#### 3. PICK DATA
|
|
58
|
+
After receiving states (if request was successful, we did not exceed limits and received at least one state)
|
|
59
|
+
it picks crucial data and filters out excessive entries: for instance objects that are on ground will be discarded.
|
|
60
|
+
At this point we already have some valuable data such as transponder hex code and flight callsign,
|
|
61
|
+
along with registered altitude and velocity.
|
|
62
|
+
#### 4. FETCH DETAILS
|
|
63
|
+
Based on previous step we enrich the data by making requests to multiple sources to retrieve information about aircraft
|
|
64
|
+
such as: registration code, manufacturer and model, age (based on built date) and operating airline/registered owner.
|
|
65
|
+
It continues with the route and send multiple requests as well to receive information about departure,
|
|
66
|
+
destination and for some cases midpoint airports. Finally, it searches for the aircraft photo.
|
|
67
|
+
#### 5. BUILD RESULT
|
|
68
|
+
This is the last step where all data being returned in declared format.
|
|
69
|
+
|
|
70
|
+
## Installation
|
|
71
|
+
```bash
|
|
72
|
+
pip install plane-above
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
## Usage
|
|
77
|
+
First, import library, create instance and pass your values of latitude & longitude as a tuple:
|
|
78
|
+
```python
|
|
79
|
+
>>> from plane_above import PlaneAbove
|
|
80
|
+
>>> pa = PlaneAbove((52.4573212, 5.5301535))
|
|
81
|
+
```
|
|
82
|
+
Optionally, you can pass a distance attribute (in km, 15 by default):
|
|
83
|
+
```python
|
|
84
|
+
>>> pa = PlaneAbove((52.4573212, 5.5301535), distance=20)
|
|
85
|
+
```
|
|
86
|
+
Great, by this time we already know what is happening above that point, so let's check it:
|
|
87
|
+
```python
|
|
88
|
+
>>> pa.spotted.success
|
|
89
|
+
True
|
|
90
|
+
>>> pa.spotted.how_many
|
|
91
|
+
2
|
|
92
|
+
```
|
|
93
|
+
In cases when there are no plane detected, or we failed to retrieve information you will get
|
|
94
|
+
```True, 0``` and ```False, 0``` respectively. Let's go further and see what are that 2 flying objects:
|
|
95
|
+
```python
|
|
96
|
+
>>> for plane in pa.fetch():
|
|
97
|
+
... print(f"{plane.aircraft.manufacturer} {plane.aircraft.model}
|
|
98
|
+
... flying to {plane.route.destination.name} is {plane.state.altitude} meters above you!")
|
|
99
|
+
|
|
100
|
+
Boeing 777 212ER flying to London Heathrow Airport is 11232 meters above you!
|
|
101
|
+
Airbus A330 342 flying to Brussels Airport (Zaventem Airport) is 1006 meters above you!
|
|
102
|
+
```
|
|
103
|
+
If you live in acynchronious environment, call ```async.fetch()``` instead:
|
|
104
|
+
```python
|
|
105
|
+
>>> async for plane in pa.async_fetch():
|
|
106
|
+
...
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## Object Reference
|
|
110
|
+
<details>
|
|
111
|
+
<summary>Aircraft</summary>
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
class Aircraft:
|
|
115
|
+
registration: str
|
|
116
|
+
manufacturer: str
|
|
117
|
+
model: str
|
|
118
|
+
operator: str
|
|
119
|
+
age: float
|
|
120
|
+
```
|
|
121
|
+
Note: don’t take age 0.0 as a really new plane, as age is might be missing from our sources;
|
|
122
|
+
all strings might be empty strings.
|
|
123
|
+
</details>
|
|
124
|
+
|
|
125
|
+
<details>
|
|
126
|
+
<summary>Photo</summary>
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
class Photo:
|
|
130
|
+
image_url: str
|
|
131
|
+
origin_url: str
|
|
132
|
+
photographer: str
|
|
133
|
+
```
|
|
134
|
+
Note: all strings might be empty strings.
|
|
135
|
+
</details>
|
|
136
|
+
|
|
137
|
+
<details>
|
|
138
|
+
<summary>Airport</summary>
|
|
139
|
+
|
|
140
|
+
```python
|
|
141
|
+
class Airport:
|
|
142
|
+
iata: str = "N/A"
|
|
143
|
+
name: str = "Unknown airport"
|
|
144
|
+
country_code: str = ""
|
|
145
|
+
```
|
|
146
|
+
</details>
|
|
147
|
+
|
|
148
|
+
<details>
|
|
149
|
+
<summary>Route</summary>
|
|
150
|
+
|
|
151
|
+
```python
|
|
152
|
+
class Route:
|
|
153
|
+
departure: Airport
|
|
154
|
+
destination: Airport
|
|
155
|
+
stops: list[Airport]
|
|
156
|
+
airline: str | None = None
|
|
157
|
+
```
|
|
158
|
+
</details>
|
|
159
|
+
|
|
160
|
+
<details>
|
|
161
|
+
<summary>State</summary>
|
|
162
|
+
|
|
163
|
+
```python
|
|
164
|
+
class State:
|
|
165
|
+
altitude: int
|
|
166
|
+
velocity: int
|
|
167
|
+
```
|
|
168
|
+
Note: velocity and/or altitude can be 0 if no data from transponder received.
|
|
169
|
+
</details>
|
|
170
|
+
|
|
171
|
+
<details>
|
|
172
|
+
<summary>Plane</summary>
|
|
173
|
+
|
|
174
|
+
```python
|
|
175
|
+
class Plane:
|
|
176
|
+
icao24: str
|
|
177
|
+
callsign: str
|
|
178
|
+
country_code: str
|
|
179
|
+
aircraft: Aircraft
|
|
180
|
+
route: Route
|
|
181
|
+
state: State
|
|
182
|
+
photo: Photo
|
|
183
|
+
```
|
|
184
|
+
</details>
|
|
185
|
+
|
|
186
|
+
## Limitations
|
|
187
|
+
OpenSky Network allows to use it's api anonymously, but with limits.
|
|
188
|
+
If you wish to extend your usage - consider obtaining ```client_id``` and ```client_secret```
|
|
189
|
+
[here](https://openskynetwork.github.io/opensky-api/rest.html#authentication). Then you pass it like this:
|
|
190
|
+
```python
|
|
191
|
+
>>> pa = PlaneAbove((52.4573212, 5.5301535), osn_id="your_client_id", osn_secret="your_client_secret")
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
In some cases your calls to OpenSky Network might be timed-out without any specific reason, that's due to OSN
|
|
195
|
+
blocking policies and you probably would like to utilize proxy here as a workaround:
|
|
196
|
+
```python
|
|
197
|
+
>>> pa = PlaneAbove((52.4573212, 5.5301535), osn_proxy="http://username:password@host:port")
|
|
198
|
+
```
|
|
199
|
+
Please note that this will be used for making an OSN request only; other sources won't be called with that proxy.
|
|
200
|
+
|
|
201
|
+
One of our photo sources has a strict policy about making requests with
|
|
202
|
+
[unique and descriptive user-agents](https://www.planespotters.net/photo/api). That's doable with an extra param:
|
|
203
|
+
```python
|
|
204
|
+
>>> pa = PlaneAbove((52.4573212, 5.5301535), ps_user_agent="YourApp/1.0 (+https://example.com/contact)")
|
|
205
|
+
```
|
|
206
|
+
However, you are free to skip it as it won't affect other photo sources.
|
|
207
|
+
|
|
208
|
+
## Credits
|
|
209
|
+
* Basic and essential: https://opensky-network.org/
|
|
210
|
+
* Aircraft data & photo, route & airport details: https://hexdb.io/
|
|
211
|
+
* Aircraft data, route & airport details: https://adsbdb.com/
|
|
212
|
+
* Aircraft photo, airport details: https://airport-data.com/
|
|
213
|
+
* Aircraft data: https://flightdb.net/
|
|
214
|
+
* Aircraft photo: https://planespotters.net/
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
<img src="logo.svg" alt="plane-above" width="200">
|
|
2
|
+
|
|
3
|
+
# Plane Above
|
|
4
|
+
Plane Above is a python package featuring a convinient way to retrieve planes above certain point.
|
|
5
|
+
|
|
6
|
+
<details>
|
|
7
|
+
<summary>One notable usage example...</summary>
|
|
8
|
+
|
|
9
|
+
Try <a href="https://t.me/plane_above_bot">Plane Above</a> telegram bot.
|
|
10
|
+
<img src="tg.png" alt="plane-above-bot" width="400">
|
|
11
|
+
</details>
|
|
12
|
+
|
|
13
|
+
## Before we start...
|
|
14
|
+
To collect data, this library utilizes various sources. They are open and maintained by enthusiasts mostly.
|
|
15
|
+
So, use responsively! Don't push the limits and take actions with care.
|
|
16
|
+
Please mind that data might be missing, incorrect or outdated.
|
|
17
|
+
|
|
18
|
+
## How it works
|
|
19
|
+
_Note: here and after I would refer to any flying object as an «aircraft» or «plane»,
|
|
20
|
+
but keep in mind that it also includes helicopters, gliders, rotorcraft and even balloons._
|
|
21
|
+
#### 1. BUILD SEARCHING AREA
|
|
22
|
+
With given latitude and longitude of point it builds area of search by calculating bounding box coordinates
|
|
23
|
+
based on required distance from point.
|
|
24
|
+
#### 2. RETRIEVE STATES
|
|
25
|
+
Next it makes a request to OpenSky API to retrieve all states in searching area.
|
|
26
|
+
Each state represents [state vector](https://openskynetwork.github.io/opensky-api/index.html#state-vectors)
|
|
27
|
+
and contains information of ADS-B messages from transponder installed in aircraft.
|
|
28
|
+
#### 3. PICK DATA
|
|
29
|
+
After receiving states (if request was successful, we did not exceed limits and received at least one state)
|
|
30
|
+
it picks crucial data and filters out excessive entries: for instance objects that are on ground will be discarded.
|
|
31
|
+
At this point we already have some valuable data such as transponder hex code and flight callsign,
|
|
32
|
+
along with registered altitude and velocity.
|
|
33
|
+
#### 4. FETCH DETAILS
|
|
34
|
+
Based on previous step we enrich the data by making requests to multiple sources to retrieve information about aircraft
|
|
35
|
+
such as: registration code, manufacturer and model, age (based on built date) and operating airline/registered owner.
|
|
36
|
+
It continues with the route and send multiple requests as well to receive information about departure,
|
|
37
|
+
destination and for some cases midpoint airports. Finally, it searches for the aircraft photo.
|
|
38
|
+
#### 5. BUILD RESULT
|
|
39
|
+
This is the last step where all data being returned in declared format.
|
|
40
|
+
|
|
41
|
+
## Installation
|
|
42
|
+
```bash
|
|
43
|
+
pip install plane-above
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
## Usage
|
|
48
|
+
First, import library, create instance and pass your values of latitude & longitude as a tuple:
|
|
49
|
+
```python
|
|
50
|
+
>>> from plane_above import PlaneAbove
|
|
51
|
+
>>> pa = PlaneAbove((52.4573212, 5.5301535))
|
|
52
|
+
```
|
|
53
|
+
Optionally, you can pass a distance attribute (in km, 15 by default):
|
|
54
|
+
```python
|
|
55
|
+
>>> pa = PlaneAbove((52.4573212, 5.5301535), distance=20)
|
|
56
|
+
```
|
|
57
|
+
Great, by this time we already know what is happening above that point, so let's check it:
|
|
58
|
+
```python
|
|
59
|
+
>>> pa.spotted.success
|
|
60
|
+
True
|
|
61
|
+
>>> pa.spotted.how_many
|
|
62
|
+
2
|
|
63
|
+
```
|
|
64
|
+
In cases when there are no plane detected, or we failed to retrieve information you will get
|
|
65
|
+
```True, 0``` and ```False, 0``` respectively. Let's go further and see what are that 2 flying objects:
|
|
66
|
+
```python
|
|
67
|
+
>>> for plane in pa.fetch():
|
|
68
|
+
... print(f"{plane.aircraft.manufacturer} {plane.aircraft.model}
|
|
69
|
+
... flying to {plane.route.destination.name} is {plane.state.altitude} meters above you!")
|
|
70
|
+
|
|
71
|
+
Boeing 777 212ER flying to London Heathrow Airport is 11232 meters above you!
|
|
72
|
+
Airbus A330 342 flying to Brussels Airport (Zaventem Airport) is 1006 meters above you!
|
|
73
|
+
```
|
|
74
|
+
If you live in acynchronious environment, call ```async.fetch()``` instead:
|
|
75
|
+
```python
|
|
76
|
+
>>> async for plane in pa.async_fetch():
|
|
77
|
+
...
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## Object Reference
|
|
81
|
+
<details>
|
|
82
|
+
<summary>Aircraft</summary>
|
|
83
|
+
|
|
84
|
+
```python
|
|
85
|
+
class Aircraft:
|
|
86
|
+
registration: str
|
|
87
|
+
manufacturer: str
|
|
88
|
+
model: str
|
|
89
|
+
operator: str
|
|
90
|
+
age: float
|
|
91
|
+
```
|
|
92
|
+
Note: don’t take age 0.0 as a really new plane, as age is might be missing from our sources;
|
|
93
|
+
all strings might be empty strings.
|
|
94
|
+
</details>
|
|
95
|
+
|
|
96
|
+
<details>
|
|
97
|
+
<summary>Photo</summary>
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
class Photo:
|
|
101
|
+
image_url: str
|
|
102
|
+
origin_url: str
|
|
103
|
+
photographer: str
|
|
104
|
+
```
|
|
105
|
+
Note: all strings might be empty strings.
|
|
106
|
+
</details>
|
|
107
|
+
|
|
108
|
+
<details>
|
|
109
|
+
<summary>Airport</summary>
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
class Airport:
|
|
113
|
+
iata: str = "N/A"
|
|
114
|
+
name: str = "Unknown airport"
|
|
115
|
+
country_code: str = ""
|
|
116
|
+
```
|
|
117
|
+
</details>
|
|
118
|
+
|
|
119
|
+
<details>
|
|
120
|
+
<summary>Route</summary>
|
|
121
|
+
|
|
122
|
+
```python
|
|
123
|
+
class Route:
|
|
124
|
+
departure: Airport
|
|
125
|
+
destination: Airport
|
|
126
|
+
stops: list[Airport]
|
|
127
|
+
airline: str | None = None
|
|
128
|
+
```
|
|
129
|
+
</details>
|
|
130
|
+
|
|
131
|
+
<details>
|
|
132
|
+
<summary>State</summary>
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
class State:
|
|
136
|
+
altitude: int
|
|
137
|
+
velocity: int
|
|
138
|
+
```
|
|
139
|
+
Note: velocity and/or altitude can be 0 if no data from transponder received.
|
|
140
|
+
</details>
|
|
141
|
+
|
|
142
|
+
<details>
|
|
143
|
+
<summary>Plane</summary>
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
class Plane:
|
|
147
|
+
icao24: str
|
|
148
|
+
callsign: str
|
|
149
|
+
country_code: str
|
|
150
|
+
aircraft: Aircraft
|
|
151
|
+
route: Route
|
|
152
|
+
state: State
|
|
153
|
+
photo: Photo
|
|
154
|
+
```
|
|
155
|
+
</details>
|
|
156
|
+
|
|
157
|
+
## Limitations
|
|
158
|
+
OpenSky Network allows to use it's api anonymously, but with limits.
|
|
159
|
+
If you wish to extend your usage - consider obtaining ```client_id``` and ```client_secret```
|
|
160
|
+
[here](https://openskynetwork.github.io/opensky-api/rest.html#authentication). Then you pass it like this:
|
|
161
|
+
```python
|
|
162
|
+
>>> pa = PlaneAbove((52.4573212, 5.5301535), osn_id="your_client_id", osn_secret="your_client_secret")
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
In some cases your calls to OpenSky Network might be timed-out without any specific reason, that's due to OSN
|
|
166
|
+
blocking policies and you probably would like to utilize proxy here as a workaround:
|
|
167
|
+
```python
|
|
168
|
+
>>> pa = PlaneAbove((52.4573212, 5.5301535), osn_proxy="http://username:password@host:port")
|
|
169
|
+
```
|
|
170
|
+
Please note that this will be used for making an OSN request only; other sources won't be called with that proxy.
|
|
171
|
+
|
|
172
|
+
One of our photo sources has a strict policy about making requests with
|
|
173
|
+
[unique and descriptive user-agents](https://www.planespotters.net/photo/api). That's doable with an extra param:
|
|
174
|
+
```python
|
|
175
|
+
>>> pa = PlaneAbove((52.4573212, 5.5301535), ps_user_agent="YourApp/1.0 (+https://example.com/contact)")
|
|
176
|
+
```
|
|
177
|
+
However, you are free to skip it as it won't affect other photo sources.
|
|
178
|
+
|
|
179
|
+
## Credits
|
|
180
|
+
* Basic and essential: https://opensky-network.org/
|
|
181
|
+
* Aircraft data & photo, route & airport details: https://hexdb.io/
|
|
182
|
+
* Aircraft data, route & airport details: https://adsbdb.com/
|
|
183
|
+
* Aircraft photo, airport details: https://airport-data.com/
|
|
184
|
+
* Aircraft data: https://flightdb.net/
|
|
185
|
+
* Aircraft photo: https://planespotters.net/
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from typing import NamedTuple
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from collections.abc import Iterator, AsyncIterator
|
|
5
|
+
|
|
6
|
+
import httpx
|
|
7
|
+
|
|
8
|
+
from .osn import OSN, FlyingObject
|
|
9
|
+
from .route import Route, Airport, FlightRoute
|
|
10
|
+
from .utils import log
|
|
11
|
+
from .static import DEFAULT_DISTANCE_FROM_POINT
|
|
12
|
+
from .aircraft import Photo, Aircraft, AircraftPhoto, AircraftDetails
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True)
|
|
16
|
+
class State:
|
|
17
|
+
altitude: int
|
|
18
|
+
velocity: int
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Plane(NamedTuple):
|
|
22
|
+
icao24: str
|
|
23
|
+
callsign: str
|
|
24
|
+
country_code: str
|
|
25
|
+
aircraft: Aircraft
|
|
26
|
+
route: Route
|
|
27
|
+
state: State
|
|
28
|
+
photo: Photo
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class Spotted(NamedTuple):
|
|
32
|
+
objects_raw: list
|
|
33
|
+
objects_filtered: list[FlyingObject]
|
|
34
|
+
success: bool
|
|
35
|
+
how_many: int
|
|
36
|
+
errors: list
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class PlaneAbove:
|
|
40
|
+
"""Retrieving detailed information about flying objects above given point.
|
|
41
|
+
|
|
42
|
+
Fetches aircraft details, flight routes, and photos for each spotted object,
|
|
43
|
+
combining data from multiple sources into structured `Plane` instances.
|
|
44
|
+
|
|
45
|
+
Attributes:
|
|
46
|
+
spotted (Spotted): Container with flying objects and additional info.
|
|
47
|
+
_ps_user_agent (str): User-Agent string for PlaneSpotters API requests.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
def __init__(
|
|
51
|
+
self,
|
|
52
|
+
point: tuple[float, float],
|
|
53
|
+
*,
|
|
54
|
+
distance: int | float = DEFAULT_DISTANCE_FROM_POINT,
|
|
55
|
+
osn_id: str = "",
|
|
56
|
+
osn_secret: str = "",
|
|
57
|
+
osn_proxy: str | None = None,
|
|
58
|
+
ps_user_agent: str = "",
|
|
59
|
+
):
|
|
60
|
+
"""Initialize PlaneAbove client with search parameters and API credentials.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
point: Geographic coordinates as (latitude, longitude).
|
|
64
|
+
distance: Search radius in kilometers from the given point.
|
|
65
|
+
osn_id: OpenSky Network API identifier.
|
|
66
|
+
osn_secret: OpenSky Network API secret.
|
|
67
|
+
osn_proxy: Optional proxy URL for OpenSky Network requests.
|
|
68
|
+
ps_user_agent: User-Agent string for PlaneSpotters API requests.
|
|
69
|
+
"""
|
|
70
|
+
self._ps_user_agent = ps_user_agent
|
|
71
|
+
self.spotted = Spotted(*OSN.get_flying_objects(point, distance, osn_id, osn_secret, osn_proxy), errors=[])
|
|
72
|
+
|
|
73
|
+
async def _retrieve_data(self, f_object: FlyingObject) -> tuple[FlyingObject, Aircraft, Route, Photo]:
|
|
74
|
+
async with httpx.AsyncClient() as async_client:
|
|
75
|
+
try:
|
|
76
|
+
aircraft_details, route = await asyncio.gather(
|
|
77
|
+
AircraftDetails.get_details(async_client, f_object.icao24),
|
|
78
|
+
FlightRoute.get_route(async_client, f_object.callsign),
|
|
79
|
+
)
|
|
80
|
+
aircraft, photo = aircraft_details
|
|
81
|
+
if not all((photo.image_url, photo.origin_url)):
|
|
82
|
+
photo = await AircraftPhoto.get_photo(
|
|
83
|
+
async_client,
|
|
84
|
+
f_object.icao24,
|
|
85
|
+
aircraft.registration,
|
|
86
|
+
self._ps_user_agent,
|
|
87
|
+
)
|
|
88
|
+
return f_object, aircraft, route, photo
|
|
89
|
+
|
|
90
|
+
except Exception as exc:
|
|
91
|
+
log.error(f" ✈ Exception occurred for plane {f_object.icao24}: {exc}")
|
|
92
|
+
self.spotted.errors.append((f"{f_object.icao24=}", exc))
|
|
93
|
+
return (
|
|
94
|
+
f_object,
|
|
95
|
+
Aircraft(registration="", manufacturer="", model="", operator="", age=0.0),
|
|
96
|
+
Route(departure=Airport(), destination=Airport(), stops=[]),
|
|
97
|
+
Photo(image_url="", origin_url="", photographer=""),
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
@staticmethod
|
|
101
|
+
def _collect_data(f_object: FlyingObject, aircraft: Aircraft, route: Route, photo: Photo) -> Plane:
|
|
102
|
+
return Plane(
|
|
103
|
+
icao24=f_object.icao24,
|
|
104
|
+
callsign=f_object.callsign,
|
|
105
|
+
country_code=f_object.country_code,
|
|
106
|
+
aircraft=aircraft,
|
|
107
|
+
route=route,
|
|
108
|
+
state=State(velocity=f_object.velocity, altitude=f_object.altitude),
|
|
109
|
+
photo=photo,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
def fetch(self) -> Iterator[Plane]:
|
|
113
|
+
"""Retrieve detailed Plane data for all spotted flying objects.
|
|
114
|
+
|
|
115
|
+
Iterates over filtered flying objects, fetches data for each and yields populated Plane instances.
|
|
116
|
+
|
|
117
|
+
Yields:
|
|
118
|
+
Plane: Structured aircraft data for each spotted object.
|
|
119
|
+
|
|
120
|
+
Raises:
|
|
121
|
+
RuntimeError: If called from an existing async context.
|
|
122
|
+
"""
|
|
123
|
+
try:
|
|
124
|
+
for f_object in self.spotted.objects_filtered:
|
|
125
|
+
yield self._collect_data(*asyncio.run(self._retrieve_data(f_object)))
|
|
126
|
+
except RuntimeError as exc:
|
|
127
|
+
log.error(" ✈ For asynchronous environment use async_fetch method")
|
|
128
|
+
raise exc
|
|
129
|
+
|
|
130
|
+
async def async_fetch(self) -> AsyncIterator[Plane]:
|
|
131
|
+
"""Async retrieve detailed Plane data for all spotted flying objects.
|
|
132
|
+
|
|
133
|
+
Iterates over filtered flying objects, fetches data for each and yields populated Plane instances.
|
|
134
|
+
|
|
135
|
+
Yields:
|
|
136
|
+
Plane: Structured aircraft data for each spotted object.
|
|
137
|
+
"""
|
|
138
|
+
for f_object in self.spotted.objects_filtered:
|
|
139
|
+
yield self._collect_data(*await self._retrieve_data(f_object))
|