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.
@@ -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,6 @@
1
+ from .route import Route, Airport
2
+ from .aircraft import Photo, Aircraft
3
+ from ._plane_above import Plane, State, PlaneAbove
4
+
5
+ __all__ = ["Aircraft", "Airport", "Photo", "Plane", "PlaneAbove", "Route", "State"]
6
+ __version__ = "0.1.0"
@@ -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))