File Coverage

lib/WebService/OpenSky.pm
Criterion Covered Total %
statement 119 123 96.7
branch 44 68 64.7
condition 4 12 33.3
subroutine 20 20 100.0
pod 6 6 100.0
total 193 229 84.2


line stmt bran cond sub pod time code
1             package WebService::OpenSky;
2              
3             # ABSTRACT: Perl interface to the OpenSky Network API
4              
5             # see also https://github.com/openskynetwork/opensky-api
6 8     8   1337096 use v5.20.0;
  8         136  
7 8     8   2951 use WebService::OpenSky::Moose;
  8         33  
  8         39  
8 8         322 use WebService::OpenSky::Types qw(
9             ArrayRef
10             Bool
11             Dict
12             HashRef
13             InstanceOf
14             Int
15             Latitude
16             Longitude
17             NonEmptyStr
18             Num
19             Optional
20             Undef
21 8     8   1638282 );
  8         15111  
22 8     8   44751 use WebService::OpenSky::Response::States;
  8         26  
  8         510  
23 8     8   3534 use WebService::OpenSky::Response::Flights;
  8         33  
  8         407  
24 8     8   3492 use WebService::OpenSky::Response::FlightTrack;
  8         26  
  8         411  
25 8     8   64 use PerlX::Maybe;
  8         19  
  8         103  
26 8     8   5344 use Config::INI::Reader;
  8         70194  
  8         334  
27              
28 8     8   4749 use Mojo::UserAgent;
  8         3563337  
  8         176  
29 8     8   797 use Mojo::URL;
  8         22  
  8         40  
30 8     8   440 use Mojo::JSON qw( decode_json );
  8         16  
  8         517  
31 8     8   54 use Type::Params -sigs;
  8         14  
  8         193  
32              
33             our $VERSION = '0.4';
34              
35             param config => (
36             is => 'ro',
37             isa => NonEmptyStr,
38             default => method() { $ENV{HOME} . '/.openskyrc' },
39             );
40              
41             param [qw/debug raw testing/] => (
42             isa => Bool,
43             default => 0,
44             );
45              
46             field _config_data => (
47             isa => HashRef,
48             default => method() {
49             my $file = $self->config // '';
50             if ( !-e $file ) {
51             if ($self->testing # if we're testing
52             || ( $self->_has_username && $self->_has_password ) # or we have a username and password
53             || ( $ENV{OPENSKY_USERNAME} && $ENV{OPENSKY_PASSWORD} )
54             ) # even if they're from %ENV
55             {
56             return {}; # then we don't need a config file
57             }
58             croak("Config file '$file' does not exist");
59             }
60             Config::INI::Reader->read_file( $self->config );
61             },
62             );
63              
64             field _last_request_time => (
65             isa => HashRef [Int],
66             default => method() {
67             return {
68             '/states/all' => 0,
69             '/states/own' => 0,
70             };
71             }
72             );
73              
74             field _ua => (
75             isa => InstanceOf ['Mojo::UserAgent'],
76             default => method() { Mojo::UserAgent->new },
77             );
78              
79             param _username => (
80             isa => NonEmptyStr,
81             lazy => 1,
82             init_arg => 'username',
83             predicate => '_has_username',
84             default => method() { $ENV{OPENSKY_USERNAME} // $self->_config_data->{opensky}{username} },
85             );
86              
87             param _password => (
88             isa => NonEmptyStr,
89             lazy => 1,
90             init_arg => 'password',
91             predicate => '_has_password',
92             default => method() { $ENV{OPENSKY_PASSWORD} // $self->_config_data->{opensky}{password} },
93             );
94              
95             param _base_url => (
96             init_arg => 'base_url',
97             isa => NonEmptyStr,
98             lazy => 1,
99             default => method() {
100             $self->_config_data->{_}{base_url} // 'https://opensky-network.org/api';
101             },
102             );
103              
104             field limit_remaining => (
105             isa => Int | Undef,
106             writer => '_set_limit_remaining',
107             default => undef,
108             );
109              
110             signature_for get_states => (
111             method => 1,
112             named => [
113             time => Optional [Num], { default => 0 },
114             icao24 => Optional [ NonEmptyStr | ArrayRef [NonEmptyStr] ],
115             bbox => Optional [
116             Dict [
117             lamin => Latitude,
118             lomin => Longitude,
119             lamax => Latitude,
120             lomax => Longitude,
121             ],
122             { default => {} },
123             ],
124             extended => Optional [Bool],
125             ],
126             named_to_list => 1,
127             );
128              
129             method get_states( $seconds, $icao24, $bbox, $extended ) {
130             my %params = (
131             maybe time => $seconds,
132             maybe icao24 => $icao24,
133             maybe extended => $extended,
134             );
135             if ( keys $bbox->%* ) {
136             $params{$_} = $bbox->{$_} for qw( lamin lomin lamax lomax );
137             }
138              
139             return $self->_get_response(
140             route => '/states/all',
141             params => \%params,
142             class => 'WebService::OpenSky::Response::States',
143              
144             # rate_limit_noauth => 10,
145             # rate_limit_auth => 5,
146             );
147             }
148              
149             signature_for get_my_states => (
150             method => 1,
151             named => [
152             time => Optional [Num], { default => 0 },
153             icao24 => Optional [ NonEmptyStr | ArrayRef [NonEmptyStr] ],
154             serials => Optional [ NonEmptyStr | ArrayRef [NonEmptyStr] ],
155             ],
156             named_to_list => 1,
157             );
158              
159             method get_my_states( $seconds, $icao24, $serials ) {
160             my %params = (
161             extended => 1,
162             maybe time => $seconds,
163             maybe icao24 => $icao24,
164             maybe serials => $serials,
165             );
166             return $self->_get_response(
167             route => '/states/own',
168             params => \%params,
169             class => 'WebService::OpenSky::Response::States',
170              
171             # rate_limit_noauth => undef,
172             # rate_limit_auth => 1,
173             );
174             }
175              
176 4 50   4 1 13228 method get_flights_from_interval( $begin, $end ) {
  4 50       10  
  4         9  
  4         7  
  4         6  
177 4 100       9 if ( $begin >= $end ) {
178 1         25 croak 'The end time must be greater than or equal to the start time.';
179             }
180 3 100       9 if ( ( $end - $begin ) > 7200 ) {
181 1         9 croak 'The time interval must be smaller than two hours.';
182             }
183              
184 2         6 my %params = ( begin => $begin, end => $end );
185 2         10 return $self->_get_response(
186             route => '/flights/all',
187             params => \%params,
188             class => 'WebService::OpenSky::Response::Flights',
189             );
190             }
191              
192 4 50   4 1 14718 method get_flights_by_aircraft( $icao24, $begin, $end ) {
  4 50       15  
  4         7  
  4         11  
  4         4  
193 4 100       12 if ( $begin >= $end ) {
194 1         28 croak 'The end time must be greater than or equal to the start time.';
195             }
196 3 100       12 if ( ( $end - $begin ) > 2592 * 1e3 ) {
197 1         18 croak 'The time interval must be smaller than 30 days.';
198             }
199              
200 2         12 my %params = ( icao24 => $icao24, begin => $begin, end => $end );
201 2         13 return $self->_get_response(
202             route => '/flights/aircraft',
203             params => \%params,
204             class => 'WebService::OpenSky::Response::Flights',
205             );
206             }
207              
208 4 50   4 1 12053 method get_arrivals_by_airport( $airport, $begin, $end ) {
  4 50       11  
  4         9  
  4         9  
  4         6  
209 4 100       10 if ( $begin >= $end ) {
210 1         19 croak 'The end time must be greater than or equal to the start time.';
211             }
212 3 100       11 if ( ( $end - $begin ) > 604800 ) {
213 1         9 croak 'The time interval must be smaller than 7 days.';
214             }
215              
216 2         12 my %params = ( airport => $airport, begin => $begin, end => $end );
217 2         14 return $self->_get_response(
218             route => '/flights/arrival',
219             params => \%params,
220             class => 'WebService::OpenSky::Response::Flights',
221             );
222             }
223              
224 4 50   4 1 12712 method get_departures_by_airport( $airport, $begin, $end ) {
  4 50       12  
  4         7  
  4         8  
  4         5  
225 4 100       10 if ( $begin >= $end ) {
226 1         49 croak 'The end time must be greater than or equal to the start time.';
227             }
228 3 100       9 if ( ( $end - $begin ) > 604800 ) {
229 1         10 croak 'The time interval must be smaller than 7 days.';
230             }
231              
232 2         10 my %params = ( airport => $airport, begin => $begin, end => $end );
233 2         10 return $self->_get_response(
234             route => '/flights/departure',
235             params => \%params,
236             class => 'WebService::OpenSky::Response::Flights',
237             );
238             }
239              
240 2 50   2 1 18191 method get_track_by_aircraft( $icao24, $time ) {
  2 50       8  
  2         6  
  2         6  
  2         4  
241 2 50 33     18 if ( $time != 0 && ( time - $time ) > 2592 * 1e3 ) {
242 0         0 croak 'It is not possible to access flight tracks from more than 30 days in the past.';
243             }
244              
245 2         11 my %params = ( icao24 => $icao24, time => $time );
246 2         13 return $self->_get_response(
247             route => '/tracks/all',
248             params => \%params,
249             class => 'WebService::OpenSky::Response::FlightTrack',
250             );
251             }
252              
253             signature_for _get_response => (
254             method => 1,
255             named => [
256             route => NonEmptyStr,
257             params => Optional [HashRef],
258             credits => Optional [Bool],
259             class => Optional [NonEmptyStr],
260             no_auth_required => Optional [Bool],
261             ],
262             named_to_list => 1,
263             );
264              
265             method _get_response( $route, $params, $credits, $response_class, $no_auth_required ) {
266             my $url = $self->_url( $route, $params, $no_auth_required );
267              
268             if ( !$self->testing ) {
269              
270             # XXX Ugh. I'd like to use attributes to attach metadata to the
271             # methods, but with the attribute order switch, I can't. So I'm
272             # going to leave this ugly hack here.
273             my $method
274             = $route eq '/states/all' ? 'get_states'
275             : $route eq '/states/own' ? 'get_my_states'
276             : undef;
277             if ($method) {
278             if ( my $delay_remaining = $self->delay_remaining($method) ) {
279             carp("You have to wait $delay_remaining seconds before you can call $method again.");
280             return;
281             }
282             }
283              
284             my $limit_remaining = $self->limit_remaining;
285              
286             # if it's not defined, we haven't made an API call yet
287             if ( defined $limit_remaining && !$limit_remaining ) {
288             carp("You have no API credits left for $route. See https://openskynetwork.github.io/opensky-api/rest.html#limitations");
289             return;
290             }
291             }
292              
293             my $response = $self->_GET($url);
294             my $remaining = $response->headers->header('X-Rate-Limit-Remaining');
295              
296             $self->_debug( $response->headers->to_string . "\n" );
297              
298             # not all requests cost credits, so we only want to set the limit if
299             # $remaining is defined
300             $self->_set_limit_remaining($remaining) if !$credits && defined $remaining;
301              
302             # this is annoying. If the didn't match any criteria, the service should return a 200
303             # and an empty response. Instead, we get a 404.
304             if ( !$response->is_success && $response->code != 404 ) {
305             croak $response->to_string;
306             }
307             $self->_last_request_time->{$route} = time;
308              
309             return $remaining if $credits;
310             my $response_body = $response->body;
311             if ( $self->debug ) {
312             $self->_debug($response_body);
313             }
314             my $raw_response = $response_body ? decode_json($response_body) : undef;
315             return $response_class->new(
316             route => $route,
317             query => $params,
318             maybe raw_response => $raw_response,
319             );
320             }
321              
322 14 50   14 1 7511 method delay_remaining($method) {
  14 50       32  
  14         22  
  14         25  
  14         19  
323 14         31 state $rate_limits = {
324             'get_states' => {
325             route => '/states/all',
326             noauth => 10,
327             auth => 5,
328             },
329             'get_my_states' => {
330             route => '/states/own',
331             noauth => undef,
332             auth => 1,
333             },
334             };
335 14 100       63 my $delay = $rate_limits->{$method} or return 0;
336 6         52 my $limit = $self->limit_remaining;
337              
338             # XXX this is a bit of a hack. If we've not made any requests yet, we don't
339             # know what the limit is, so we assume they haven't made a request yet.
340             # Probably need to revisit this. I would love an API endpoint that lets me
341             # fetch the limit
342 6 100       60 return 0 if !defined $limit;
343              
344 3 50       16 return 0 if $limit <= 0;
345 3         17 my $seconds_since_last_request = time - $self->_last_request_time->{ $delay->{route} };
346              
347 3         25 my $delay_remaining;
348 3 50 33     10 if ( !$self->_password && $delay->{noauth} ) {
349 0         0 $delay = $delay->{noauth} - $seconds_since_last_request;
350             }
351             else {
352 3         27 $delay_remaining = $delay->{auth} - $seconds_since_last_request;
353             }
354 3 100       18 return $delay_remaining > 0 ? $delay_remaining : 0;
355             }
356              
357             # an easy target to override for testing
358             method _GET($url) {
359             $self->_debug("GET $url\n");
360             return $self->_ua->get($url)->res;
361             }
362              
363 13 50   13   1167 method _debug($msg) {
  13 50       45  
  13         24  
  13         36  
  13         22  
364 13 50       77 return if !$self->debug;
365 0         0 say STDERR $msg;
366             }
367              
368 14 50   14   47 method _url( $url, $params = {}, $no_auth_required = 0 ) {
  14 50       43  
  14 0       29  
  14 50       59  
  14         23  
369 14         67 my $username = $self->_username;
370 14         173 my $password = $self->_password;
371 14 50 33     202 if ( ( !$username || !$password ) && $no_auth_required ) {
      33        
372 0         0 return Mojo::URL->new( $self->_base_url . $url )->query($params);
373             }
374 14         71 $url = Mojo::URL->new( $self->_base_url . $url )->userinfo( $self->_username . ':' . $self->_password );
375 14         3238 $url->query($params);
376 14         3423 return $url;
377             }
378              
379             __END__
380              
381             =pod
382              
383             =encoding UTF-8
384              
385             =head1 NAME
386              
387             WebService::OpenSky - Perl interface to the OpenSky Network API
388              
389             =head1 VERSION
390              
391             version 0.4
392              
393             =head1 SYNOPSIS
394              
395             use WebService::OpenSky;
396              
397             my $api = WebService::OpenSky->new(
398             username => 'username',
399             password => 'password',
400             );
401              
402             my $states = $api->get_states;
403             while ( my $vector = $states->next ) {
404             say $vector->callsign;
405             }
406              
407             =head1 DESCRIPTION
408              
409             This is a Perl interface to the OpenSky Network API. It provides a simple, object-oriented
410             interface, but also allows you to fetch raw results for performance.
411              
412             This is largely based on L<the official Python
413             implementation|https://github.com/openskynetwork/opensky-api/blob/master/python/opensky_api.py>,
414             but with some changes to make it more user-friendly for Perl developers.
415              
416             =head1 CONSTRUCTOR
417              
418             Basic usage:
419              
420             my $open_sky = WebService::OpenSky->new;
421              
422             This will create an instance of the API object with no authentication. This only allows you access
423             to the C<get_states> method.
424              
425             If you want to use the other methods, you will need to provide a username and password:
426              
427             my $open_sky = WebService::OpenSky->new(
428             username => 'username',
429             password => 'password',
430             );
431              
432             You can get a username and password by registering for a free account on
433             L<OpenSky Network|https://opensky-network.org>.
434              
435             Alternatively, you can set the C<OPENSKY_USERNAME> and C<OPENSKY_PASSWORD>
436             environment variables, or create a C<.openskyrc> file in your home directory
437             with the following contents:
438              
439             [opensky]
440             username = myusername
441             password = s3cr3t
442              
443             If you'd like that file in another directory, just pass the C<config> argument:
444              
445             my $open_sky = WebService::OpenSky->new(
446             config => '/path/to/config',
447             );
448              
449             All methods return objects. However, we don't inflate the results into objects
450             until you ask for the next result. This is to avoid inflating all results if it's expensive.
451             In that case, you can ask for the raw results:
452              
453             my $open_sky = WebService::OpenSky->new->get_states;
454             my $raw = $open_sky->raw_response;
455              
456             If you are debugging why something failed, pass the C<debug> attribute to see
457             a C<STDERR> trace of the requests and responses:
458              
459             my $open_sky = WebService::OpenSky->new(
460             debug => 1,
461             );
462              
463             In the unlikely event that you need to change the base URL, you can do so:
464              
465             my $open_sky = WebService::OpenSky->new(
466             base_url => 'https://opensky-network.org/api/v2',
467             );
468              
469             The base url defaults to L<https://opensky-network.org/api>.
470              
471             =head1 METHODS
472              
473             For more insight to all methods, see L<the OpenSky API
474             documentation|https://openskynetwork.github.io/opensky-api/>.
475              
476             Note a key difference between the Python implementation and this one: the
477             Python implementation returns <None> if results are not found. For this
478             module, you will still receive the iterator, but it won't have any results.
479             This allows you to keep a consistent interface without having to check for
480             C<undef> everywhere.
481              
482             =head2 get_states
483              
484             my $states = $api->get_states;
485              
486             Returns an instance of L<WebService::OpenSky::Response::States>.
487              
488             This API call can be used to retrieve any state vector of the OpenSky. Please
489             note that rate limits apply for this call. For API calls without rate
490             limitation, see C<get_my_states>.
491              
492             By default, the above fetches all current state vectors.
493              
494             You can (optionally) request state vectors for particular airplanes or times
495             using the following request parameters:
496              
497             my $states = $api->get_states(
498             icao24 => 'abc9f3',
499             time => 1517258400,
500             );
501              
502             Both parameters are optional.
503              
504             =over 4
505              
506             =item * C<icao24>
507              
508             One or more ICAO24 transponder addresses represented by a hex string (e.g.
509             abc9f3). To filter multiple ICAO24 append the property once for each address.
510             If omitted, the state vectors of all aircraft are returned.
511              
512             =item * C<time>
513              
514             A Unix timestamp (seconds since epoch). Only state vectors after this timestamp are returned.
515              
516             =back
517              
518             In addition to that, it is possible to query a certain area defined by a
519             bounding box of WGS84 coordinates. For this purpose, add the following
520             parameters:
521              
522             my $states = $api->get_states(
523             bbox => {
524             lomin => -0.5, # lower bound for the longitude in decimal degrees
525             lamin => 51.25, # lower bound for the latitude in decimal degrees
526             lomax => 0, # upper bound for the longitude in decimal degrees
527             lamax => 51.75, # upper bound for the latitude in decimal degrees
528             },
529             );
530              
531             You can also request the category of aircraft by adding the following request parameter:
532              
533             my $states = $api->get_states(
534             extended => 1,
535             );
536              
537             Any and all of the above parameters can be combined.
538              
539             my $states = $api->get_states(
540             icao24 => 'abc9f3',
541             time => 1517258400,
542             bbox => {
543             lomin => -0.5, # lower bound for the longitude in decimal degrees
544             lamin => 51.25, # lower bound for the latitude in decimal degrees
545             lomax => 0, # upper bound for the longitude in decimal degrees
546             lamax => 51.75, # upper bound for the latitude in decimal degrees
547             },
548             extended => 1,
549             );
550              
551             =head2 get_my_states
552              
553             my $states = $api->get_my_states;
554              
555             Returns an instance of L<WebService::OpenSky::Response::States>.
556              
557             This API call can be used to retrieve state vectors for your own sensors
558             without rate limitations. Note that authentication is required for this
559             operation, otherwise you will get a 403 - Forbidden.
560              
561             By default, the above fetches all current state vectors for your states.
562             However, you can also pass arguments to fine-tune this:
563              
564             my $states = $api->get_my_states(
565             time => 1517258400,
566             icao24 => 'abc9f3',
567             serials => [ 1234, 5678 ],
568             );
569              
570             =over 4
571              
572             =item * C<time>
573              
574             The time in seconds since epoch (Unix timestamp to retrieve states for.
575             Current time will be used if omitted.
576              
577             =item * <icao24>
578              
579             One or more ICAO24 transponder addresses represented by a hex string (e.g.
580             abc9f3). To filter multiple ICAO24 append the property once for each address.
581             If omitted, the state vectors of all aircraft are returned.
582              
583             =item * C<serials>
584              
585             Retrieve only states of a subset of your receivers. You can pass this argument
586             several time to filter state of more than one of your receivers. In this case,
587             the API returns all states of aircraft that are visible to at least one of the
588             given receivers.
589              
590             =back
591              
592             =head2 C<get_arrivals_by_airport>
593              
594             my $arrivals = $api->get_arrivals_by_airport('KJFK', $start, $end);
595              
596             Returns an instance of L<WebService::OpenSky::Response::Flights>.
597              
598             Positional arguments:
599              
600             =over 4
601              
602             =item * C<airport>
603              
604             The ICAO code of the airport you want to get arrivals for.
605              
606             =item * C<start>
607              
608             The start time in seconds since epoch (Unix timestamp).
609              
610             =item * C<end>
611              
612             The end time in seconds since epoch (Unix timestamp).
613              
614             =back
615              
616             The interval between start and end time must be smaller than seven days.
617              
618             =head2 C<get_departures_by_airport>
619              
620             Identical to C<get_arrivals_by_airport>, but returns departures instead of arrivals.
621              
622             =head2 C<get_flights_by_aircraft>
623              
624             my $flights = $api->get_flights_by_aircraft($icao24, $start, $end);
625              
626             Returns an instance of L<WebService::OpenSky::Response::Flights>.
627              
628             The first argument is the lower-case ICAO24 transponder address of the aircraft you want.
629              
630             The second and third arguments are the start and end times in seconds since
631             epoch (Unix timestamp). Their interval must be equal to or less than 30 days.
632              
633             =head2 C<get_flights_from_interval>
634              
635             my $flights = $api->get_flights_from_interval($start, $end);
636              
637             Returns an instance of L<WebService::OpenSky::Response::Flights>.
638              
639             =head2 C<get_track_by_aircraft>
640              
641             my $track = $api->get_track_by_aircraft( $icao24, $start );
642              
643             Adds support for the experimental L<GET
644             /tracks|https://openskynetwork.github.io/opensky-api/rest.html#track-by-aircraft>
645             endpoint. Returns an instance of
646             L<WebService::OpenSky::Response::FlightTrack>.
647              
648             Per the OpenSky documentation, this endpoint is experimental and may be removed or simply
649             not working at any time.
650              
651             =head2 C<limit_remaining>
652              
653             my $limit = $api->limit_remaining;
654              
655             Returns the number of API credits you have left. See
656             L<https://openskynetwork.github.io/opensky-api/rest.html#limitations> for more
657             information.
658              
659             If you have not yet made a request, this method will return C<undef>.
660              
661             =head2 C<delay_remaining($method)>
662              
663             my $delay = $api->delay_remaining('get_states');
664              
665             When you call either C<get_states> or C<get_my_states>, the your calls will
666             be rate limited. This method returns the number of seconds you have to wait
667             until you can make another request. You can C<sleep> that many seconds before making
668             a new call:
669              
670             sleep $api->delay_remaining('get_states');
671              
672             If you attempt to make a request before the delay has expired, you will get a warning and
673             no request will be made.
674              
675             See
676             L<limitations|https://openskynetwork.github.io/opensky-api/rest.html#limitations>
677             for more details.
678              
679             =head1 EXAMPLES
680              
681             Perl Wikipedia, L<OpenSky Network|https://en.wikipedia.org/wiki/OpenSky_Network> is ...
682              
683             The OpenSky Network is a non-profit association based in Switzerland that
684             provides open access of flight tracking control data. It was set up as
685             a research project by several universities and government entities with
686             the goal to improve the security, reliability and efficiency of the
687             airspace. Its main function is to collect, process and store air traffic
688             control data and provide open access to this data to the public. Similar
689             to many existing flight trackers such as Flightradar24 and FlightAware,
690             the OpenSky Network consists of a multitude of sensors (currently around
691             1000, mostly concentrated in Europe and the US), which are connected to
692             the Internet by volunteers, industrial supporters, academic, and
693             governmental organizations. All collected raw data is archived in a
694             large historical database, containing over 23 trillion air traffic control
695             messages (November 2020). The database is primarily used by researchers
696             from different areas to analyze and improve air traffic control
697             technologies and processes
698              
699             =head2 Elon Musk's Jet
700              
701             However, this data can be used to track the movements of certain aircraft. For
702             example, Elon Musk's primary private jet (he has three, but this is the one he
703             mainly uses), has the ICAO24 transponder address C<a835af>. Running the
704             following code ...
705              
706             use WebService::OpenSky;
707              
708             my $musks_jet = 'a835af';
709             my $openapi = WebService::OpenSky->new;
710              
711             my $days = shift @ARGV // 7;
712             my $now = time;
713             my $then = $now - 86400 * $days; # Max 30 days
714              
715             my $flight_data = $openapi->get_flights_by_aircraft( $musks_jet, $then, $now );
716             say "Jet $musks_jet has " . $flight_data->count . " flights";
717              
718             As of this writing, that prints out:
719              
720             Jet a835af has 6 flights
721              
722             =head1 ETHICS
723              
724             There are some ethical considerations to be made when using this module. I was
725             ambivalent about writing it, but I decided to do so because I think it's
726             important to be aware of the privacy implications. However, it's also
727             important to be aware of the L<climate
728             implications|https://www.euronews.com/green/2023/03/30/wasteful-luxury-private-jet-pollution-more-than-doubles-in-europe>.
729              
730             Others are using the OpenSky API to model the amount of carbon being released
731             by the aviation industry, while others have used this public data to predict
732             corporate mergers and acquisitions. There are a wealth of reasons why this
733             data is useful, but not all of those reasons are good. Be good.
734              
735             =head1 AUTHOR
736              
737             Curtis "Ovid" Poe <curtis.poe@gmail.com>
738              
739             =head1 COPYRIGHT AND LICENSE
740              
741             This software is Copyright (c) 2023 by Curtis "Ovid" Poe.
742              
743             This is free software, licensed under:
744              
745             The Artistic License 2.0 (GPL Compatible)
746              
747             =cut