Astro-SpaceTrack
view release on metacpan or search on metacpan
lib/Astro/SpaceTrack.pm view on Meta::CPAN
use strict;
use warnings;
use Exporter;
our @ISA = qw{ Exporter };
our $VERSION = '0.181';
our @EXPORT_OK = qw{
shell
BODY_STATUS_IS_OPERATIONAL
BODY_STATUS_IS_SPARE
BODY_STATUS_IS_TUMBLING
BODY_STATUS_IS_DECAYED
ARRAY_REF
CODE_REF
HASH_REF
};
our %EXPORT_TAGS = (
ref => [ grep { m/ _REF \z /smx } @EXPORT_OK ],
status => [ grep { m/ \A BODY_STATUS_IS_ /smx } @EXPORT_OK ],
);
use Carp ();
use Getopt::Long 2.39;
use HTTP::Date ();
use HTTP::Request;
use HTTP::Response;
use HTTP::Status qw{
HTTP_PAYMENT_REQUIRED
HTTP_BAD_REQUEST
HTTP_NOT_FOUND
HTTP_I_AM_A_TEAPOT
HTTP_INTERNAL_SERVER_ERROR
HTTP_NOT_ACCEPTABLE
HTTP_NOT_MODIFIED
HTTP_OK
HTTP_PRECONDITION_FAILED
HTTP_UNAUTHORIZED
HTTP_INTERNAL_SERVER_ERROR
};
use IO::File;
use IO::Uncompress::Unzip ();
use JSON qw{};
use List::Util ();
use LWP::UserAgent; # Not in the base.
use POSIX ();
use Scalar::Util 1.07 ();
use Text::ParseWords ();
use Time::Local ();
use URI qw{};
# use URI::Escape qw{};
# Number of OIDs to retrieve at once. This is a global variable so I can
# play with it, but it is neither documented nor supported, and I
# reserve the right to change it or delete it without notice.
our $RETRIEVAL_SIZE = $ENV{SPACETRACK_RETRIEVAL_SIZE};
defined $RETRIEVAL_SIZE or $RETRIEVAL_SIZE = 200;
use constant COPACETIC => 'OK';
use constant BAD_SPACETRACK_RESPONSE =>
'Unable to parse SpaceTrack response';
use constant INVALID_CATALOG =>
'Catalog name %s invalid. Legal names are %s.';
use constant LAPSED_FUNDING => 'Funding lapsed.';
use constant LOGIN_FAILED => 'Login failed';
use constant NO_CREDENTIALS => 'Username or password not specified.';
use constant NO_CAT_ID => 'No catalog IDs specified.';
use constant NO_OBJ_NAME => 'No object name specified.';
use constant NO_RECORDS => 'No records found.';
use constant SESSION_PATH => '/';
use constant DEFAULT_SPACE_TRACK_REST_SEARCH_CLASS => 'satcat';
use constant DEFAULT_SPACE_TRACK_VERSION => 2;
# dump_headers constants.
use constant DUMP_NONE => 0; # No dump
use constant DUMP_TRACE => 0x01; # Logic trace
use constant DUMP_REQUEST => 0x02; # Request content
use constant DUMP_DRY_RUN => 0x04; # Do not execute request
use constant DUMP_COOKIE => 0x08; # Dump cookies.
use constant DUMP_RESPONSE => 0x10; # Dump response.
use constant DUMP_TRUNCATED => 0x20; # Dump with truncated content
my @dump_options;
foreach my $key ( sort keys %Astro::SpaceTrack:: ) {
$key =~ s/ \A DUMP_ //smx
or next;
push @dump_options, lc $key;
}
# Manifest constants for reference types
use constant ARRAY_REF => ref [];
use constant CODE_REF => ref sub {};
use constant HASH_REF => ref {};
# These are the Space Track version 1 retrieve Getopt::Long option
# specifications, and the descriptions of each option. These need to
# survive the retirement of Version 1 as a separate entity because I
# emulated them in the celestrak() method. I'm _NOT_
# emulating the options added in version 2 because they require parsing
# the TLE.
use constant CLASSIC_RETRIEVE_OPTIONS => [
descending => '(direction of sort)',
'end_epoch=s' => 'date',
last5 => '(ignored and deprecated)',
'sort=s' =>
"type ('catnum' or 'epoch', with 'catnum' the default)",
'start_epoch=s' => 'date',
];
use constant CELESTRAK_API_OPTIONS => [
'query=s', 'query type',
'format=s', 'data format',
];
use constant CELESTRAK_OPTIONS => [
# @{ CLASSIC_RETRIEVE_OPTIONS() }, # TODO deprecate and remove
@{ CELESTRAK_API_OPTIONS() },
];
lib/Astro/SpaceTrack.pm view on Meta::CPAN
identity => \&_mutate_identity,
iridium_status_format => \&_mutate_iridium_status_format,
max_range => \&_mutate_number,
password => \&_mutate_authen,
pretty => \&_mutate_attrib,
prompt => \&_mutate_attrib,
scheme_space_track => \&_mutate_attrib,
session_cookie => \&_mutate_spacetrack_interface,
space_track_version => \&_mutate_space_track_version,
url_iridium_status_kelso => \&_mutate_attrib,
url_iridium_status_mccants => \&_mutate_attrib,
url_iridium_status_sladen => \&_mutate_attrib,
username => \&_mutate_authen,
verbose => \&_mutate_attrib,
verify_hostname => \&_mutate_verify_hostname,
webcmd => \&_mutate_attrib,
with_name => \&_mutate_attrib,
);
my %accessor = (
cookie_expires => \&_access_spacetrack_interface,
cookie_name => \&_access_spacetrack_interface,
domain_space_track => \&_access_spacetrack_interface,
session_cookie => \&_access_spacetrack_interface,
);
foreach my $key ( keys %mutator ) {
exists $accessor{$key}
or $accessor{$key} = sub {
$_[0]->_deprecation_notice( attribute => $_[1] );
return $_[0]->{$_[1]};
};
}
# Maybe I really want a cookie_file attribute, which is used to do
# $self->{agent}->cookie_jar ({file => $self->{cookie_file}, autosave => 1}).
# We'll want to use a false attribute value to pass an empty hash. Going to
# this may imply modification of the new () method where the cookie_jar is
# defaulted and the session cookie's age is initialized.
=item $st = Astro::SpaceTrack->new ( ... )
=for html <a name="new"></a>
This method instantiates a new Space-Track accessor object. If any
arguments are passed, the C<set()> method is called on the new object,
and passed the arguments given.
For both historical and operational reasons, this method can get the
C<username> and C<password> values from multiple locations. It uses the
first defined value it finds in the following list:
=over
=item a value explicitly specified as an argument to C<new()>;
=item a value from the L<IDENTITY FILE|/IDENTITY FILE>, if the
C<identity> attribute is explicitly specified as true and
L<Config::Identity|Config::Identity> is installed;
=item a value from environment variable C<SPACETRACK_USER> if that has a
non-empty value;
=item a value from the L<IDENTITY FILE|/IDENTITY FILE>, if the
C<identity> attribute defaulted to true and
L<Config::Identity|Config::Identity> s installed;
=item a value from environment variable C<SPACETRACK_OPT>.
=back
The reason for preferring C<SPACETRACK_USER> over an identity file value
taken by default is that I have found that under Mac OS X an SSH session
does not have access to the system keyring, and
L<Config::Identity|Config::Identity> provides no other way to specify
the passphrase used to decrypt the private key. I concluded that if the
user explicitly requested an identity that it should be preferred to
anything from the environment, but that, for SSH access to be usable, I
needed to provide a source of username and password that would be taken
before the L<IDENTITY FILE|/IDENTITY FILE> was tried by default.
Proxies are taken from the environment if defined. See the ENVIRONMENT
section of the Perl LWP documentation for more information on how to
set these up.
=cut
sub new {
my ( $class, %arg ) = @_;
$class = ref $class if ref $class;
my $self = {
banner => 1, # shell () displays banner if true.
dump_headers => DUMP_NONE, # No dumping.
fallback => 0, # Do not fall back if primary source offline
filter => 0, # Filter mode.
iridium_status_format => 'kelso',
max_range => 500, # Sanity limit on range size.
password => undef, # Login password.
pretty => 0, # Pretty-format content
prompt => 'SpaceTrack> ',
scheme_space_track => 'https',
_space_track_interface => [
undef, # No such thing as version 0
undef, # Interface version 1 retured.
{ # Interface version 2
# This interface does not seem to put an expiration time
# on the cookie. But the docs say it's only good for a
# couple hours, so we need this so we can fudge
# something in when the time comes.
cookie_expires => 0,
cookie_name => 'chocolatechip',
domain_space_track => 'www.space-track.org',
session_cookie => undef,
},
],
space_track_version => DEFAULT_SPACE_TRACK_VERSION,
url_iridium_status_kelso =>
'https://celestrak.org/SpaceTrack/query/iridium.txt',
url_iridium_status_sladen =>
'http://www.rod.sladen.org.uk/iridium.htm',
username => undef, # Login username.
verbose => undef, # Verbose error messages for catalogs.
verify_hostname => 1, # Don't verify host names by default.
webcmd => undef, # Command to get web help.
with_name => undef, # True to retrieve three-line element sets.
};
bless $self, $class;
$self->set( identity => delete $arg{identity} );
$ENV{SPACETRACK_OPT} and
$self->set (grep {defined $_} split '\s+', $ENV{SPACETRACK_OPT});
# TODO this makes no sense - the first branch of the if() can never
# be executed because I already deleted $arg{identity}. But I do not
# want to execute the SPACETRACK_USER code willy-nilly -- maybe warn
# if identity is 1 and I don't have both a username and a password.
if ( defined( my $id = delete $arg{identity} ) ) {
$self->set( identity => $id );
} elsif ( $ENV{SPACETRACK_USER} ) {
my ($user, $pass) = split qr{ [:/] }smx, $ENV{SPACETRACK_USER}, 2;
'' ne $user
and '' ne $pass
or $user = $pass = undef;
$self->set (username => $user, password => $pass);
} else {
$self->set( identity => undef );
}
defined $ENV{SPACETRACK_VERIFY_HOSTNAME}
and $self->set( verify_hostname =>
$ENV{SPACETRACK_VERIFY_HOSTNAME} );
keys %arg
and $self->set( %arg );
return $self;
}
=for html <a name="amsat"></a>
=item $resp = $st->amsat ()
B<Note> that this method is non-functional as of September 8 2025
(probably earlier), because Amsat has gone to a "humans-only" policy for
their web site. It will be put through the usual deprecation cycle and
removed.
This method downloads current orbital elements from the Radio Amateur
Satellite Corporation's web page, L<https://www.amsat.org/>. This lists
satellites of interest to radio amateurs, and appears to be updated
weekly.
No Space Track account is needed to access this data. As of version
0.150 the setting of the 'with_name' attribute is honored.
You can specify options as either command-type options (e.g.
C<< amsat( '-file', 'foo.dat' ) >>) or as a leading hash reference (e.g.
C<< amsat( { file => 'foo.dat' } ) >>). If you specify the hash
reference, option names must be specified in full, without the leading
'-', and the argument list will not be parsed for command-type options.
If you specify command-type options, they may be abbreviated, as long as
the abbreviation is unique. Errors in either sort result in an exception
being thrown.
The legal options are:
-file
specifies the name of the cache file. If the data
on line are newer than the modification date of
the cache file, the cache file will be updated.
Otherwise the data will be returned from the file.
Either way the content of the file and the content
of the returned HTTP::Response object end up the
same.
On a successful return, the response object will contain headers
Pragma: spacetrack-type = orbit
Pragma: spacetrack-source = amsat
These can be accessed by C<< $st->content_type( $resp ) >> and
C<< $st->content_source( $resp ) >> respectively.
If the C<file> option was passed, the following additional header will
be provided:
Pragma: spacetrack-cache-hit = (either true or false)
This can be accessed by the C<cache_hit()> method. If this pragma is
true, the C<Last-Modified> header of the response will contain the
modification time of the file.
lib/Astro/SpaceTrack.pm view on Meta::CPAN
# Called dynamically
sub _file_opts { ## no critic (Subroutines::ProhibitUnusedPrivateSubroutines)
return [ _get_retrieve_options() ];
}
sub file {
my ($self, @args) = @_;
my ( $opt, $file ) = $self->_parse_retrieve_args( @args );
delete $self->{_pragmata};
if ( ! Scalar::Util::openhandle( $file ) ) {
-e $file or return HTTP::Response->new (
HTTP_NOT_FOUND, "Can't find file $file");
my $fh = IO::File->new($file, '<') or
return HTTP::Response->new (
HTTP_INTERNAL_SERVER_ERROR, "Can't open $file: $!");
$file = $fh;
}
local $/ = undef;
return $self->_handle_observing_list( $opt, <$file> )
}
=for html <a name="get"></a>
=item $resp = $st->get (attrib)
B<This method returns an HTTP::Response object> whose content is the value
of the given attribute. If called in list context, the second element
of the list is just the value of the attribute, for those who don't want
to winkle it out of the response object. We croak on a bad attribute name.
If this method succeeds, the response will contain header
Pragma: spacetrack-type = get
This can be accessed by C<< $st->content_type( $resp ) >>.
See L</Attributes> for the names and functions of the attributes.
=cut
# Called dynamically
sub _readline_complete_command_get { ## no critic (Subroutines::ProhibitUnusedPrivateSubroutines)
# my ( $self, $text, $line, $start, $cmd_line ) = @_;
my ( $self, $text ) = @_;
$text eq ''
and return( $self->attribute_names() );
my $re = qr/ \A \Q$text\E /smx;
return( sort grep { $_ =~ $re } $self->attribute_names() );
}
sub get {
my ( $self, $name ) = @_;
delete $self->{_pragmata};
my $code = $self->can( "_get_attr_$name" ) || $self->can( 'getv' );
my $value = $code->( $self, $name );
my $resp = HTTP::Response->new( HTTP_OK, COPACETIC, undef, $value );
$self->_add_pragmata( $resp,
'spacetrack-type' => 'get',
);
$self->__dump_response( $resp );
return wantarray ? ($resp, $value ) : $resp;
}
# Called dynamically
sub _get_attr_dump_headers { ## no critic (Subroutines::ProhibitUnusedPrivateSubroutines)
my ( $self, $name ) = @_;
my $value = $self->getv( $name );
my @opts = ( $value, '#' );
if ( $value ) {
foreach my $key ( @dump_options ) {
my $const = "DUMP_\U$key";
my $mask = __PACKAGE__->$const();
$value & $mask
and push @opts, "--$key";
}
} else {
push @opts, '--none';
}
return "@opts";
}
=for html <a name="getv"></a>
=item $value = $st->getv (attrib)
This method returns the value of the given attribute, which is what
C<get()> should have done.
See L</Attributes> for the names and functions of the attributes.
=cut
sub getv {
my ( $self, $name ) = @_;
defined $name
or Carp::croak 'No attribute name specified';
my $code = $accessor{$name}
or Carp::croak "No such attribute as '$name'";
return $code->( $self, $name );
}
=for html <a name="help"></a>
=item $resp = $st->help ()
This method exists for the convenience of the shell () method. It
always returns success, with the content being whatever it's
convenient (to the author) to include.
If the C<webcmd> attribute is set, the L<https://metacpan.org/>
web page for Astro::Satpass is launched.
If this method succeeds B<and> the webcmd attribute is not set, the
response will contain header
lib/Astro/SpaceTrack.pm view on Meta::CPAN
];
}
sub retrieve {
my ( $self, @args ) = @_;
delete $self->{_pragmata};
@args = $self->_parse_retrieve_args( @args );
my $opt = _parse_retrieve_dates( shift @args );
my $rest = $self->_convert_retrieve_options_to_rest( $opt );
@args = $self->_expand_oid_list( @args )
or return HTTP::Response->new( HTTP_PRECONDITION_FAILED, NO_CAT_ID );
my $no_execute = $self->getv( 'dump_headers' ) & DUMP_DRY_RUN;
## $rest->{orderby} = 'EPOCH desc';
my $accumulator = _accumulator_for (
$no_execute ?
( json => { pretty => 1 } ) :
( $rest->{format}, {
file => 1,
pretty => $self->getv( 'pretty' )
},
)
);
while ( @args ) {
my @batch = splice @args, 0, $RETRIEVAL_SIZE;
$rest->{NORAD_CAT_ID} = _stringify_oid_list( {
separator => ',',
range_operator => '--',
}, @batch );
my $resp = $self->spacetrack_query_v2(
basicspacedata => 'query',
_sort_rest_arguments( $rest )
);
$resp->is_success()
or $resp->code() == HTTP_I_AM_A_TEAPOT
or return $resp;
$accumulator->( $self, $resp );
}
( my $data = $accumulator->( $self ) )
or return HTTP::Response->new ( HTTP_NOT_FOUND, NO_RECORDS );
ref $data
and $data = $self->_get_json_object()->encode( $data );
$no_execute
and return HTTP::Response->new(
HTTP_I_AM_A_TEAPOT, undef, undef, $data );
my $resp = HTTP::Response->new( HTTP_OK, COPACETIC, undef,
$data );
$self->_convert_content( $resp );
$self->_add_pragmata( $resp,
'spacetrack-type' => 'orbit',
'spacetrack-source' => 'spacetrack',
'spacetrack-interface' => 2,
);
return $resp;
}
sub _convert_retrieve_options_to_rest {
my ( $self ) = @_;
my $method = "_convert_retrieve_options_to_rest_v$self->{space_track_version}";
my $code = $self->can( $method )
or Carp::confess( "Bug - method $method() not found" );
goto $code;
}
{
my %rest_sort_map = (
catnum => 'NORAD_CAT_ID',
epoch => 'EPOCH',
);
# Called dynamically
sub _convert_retrieve_options_to_rest_v2 { ## no critic (Subroutines::ProhibitUnusedPrivateSubroutines)
my ( $self, $opt ) = @_;
my %rest = (
class => 'gp',
);
if ( $opt->{start_epoch} || $opt->{end_epoch} ) {
$rest{EPOCH} = join '--', map { _rest_date( $opt->{$_} ) }
qw{ _start_epoch _end_epoch };
$rest{class} = 'gp_history';
}
$rest{orderby} = ( $rest_sort_map{$opt->{sort} || 'catnum'} ||
'NORAD_CAT_ID' )
. ( $opt->{descending} ? ' desc' : ' asc' );
if ( $opt->{since_file} ) {
$rest{FILE} = ">$opt->{since_file}";
$rest{class} = 'gp_history';
}
if ( $opt->{status} && $opt->{status} ne 'onorbit' ) {
$rest{class} = 'gp_history';
}
foreach my $name (
qw{ class format },
qw{ ECCENTRICITY FILE MEAN_MOTION OBJECT_NAME },
) {
defined $opt->{$name}
and $rest{$name} = $opt->{$name};
lib/Astro/SpaceTrack.pm view on Meta::CPAN
sub search_oid { ## no critic (RequireArgUnpacking)
## my ( $self, @args ) = @_;
splice @_, 1, 0, NORAD_CAT_ID => sub { return $_[0] };
goto &_search_rest;
}
sub _check_range {
my ( $self, $lo, $hi ) = @_;
($lo, $hi) = ($hi, $lo) if $lo > $hi;
$lo or $lo = 1; # 0 is illegal
$hi - $lo >= $self->{max_range} and do {
Carp::carp <<"EOD";
Warning - Range $lo-$hi ignored because it is greater than the
currently-set maximum of $self->{max_range}.
EOD
return;
};
return ( $lo, $hi );
}
=for html <a name="set"></a>
=item $st->set ( ... )
This is the mutator method for the object. It can be called explicitly,
but other methods as noted may call it implicitly also. It croaks if
you give it an odd number of arguments, or if given an attribute that
either does not exist or cannot be set.
For the convenience of the shell method we return a HTTP::Response
object with a success status if all goes well. But if we encounter an
error we croak.
See L</Attributes> for the names and functions of the attributes.
=cut
# Called dynamically
sub _readline_complete_command_set { ## no critic (Subroutines::ProhibitUnusedPrivateSubroutines)
# my ( $self, $text, $line, $start, $cmd_line ) = @_;
my ( undef, undef, undef, undef, $cmd_line ) = @_;
@{ $cmd_line } % 2
or return; # Can't complete arguments
goto &_readline_complete_command_get;
}
sub set { ## no critic (ProhibitAmbiguousNames)
my ($self, @args) = @_;
delete $self->{_pragmata};
while ( @args > 1 ) {
my $name = shift @args;
Carp::croak "Attribute $name may not be set. Legal attributes are ",
join (', ', sort keys %mutator), ".\n"
unless $mutator{$name};
my $value = $args[0];
$mutator{$name}->( $self, $name, $value, \@args );
shift @args;
}
@args
and Carp::croak __PACKAGE__, "->set() specifies no value for @args";
my $resp = HTTP::Response->new( HTTP_OK, COPACETIC, undef, COPACETIC );
$self->_add_pragmata( $resp,
'spacetrack-type' => 'set',
);
$self->__dump_response( $resp );
return $resp;
}
=for html <a name="shell"></a>
=item $st->shell ()
This method implements a simple shell. Any public method name except
'new' or 'shell' is a command, and its arguments if any are parameters.
We use L<Text::ParseWords|Text::ParseWords> to parse the line, and blank
lines or lines beginning with a hash mark ('#') are ignored. Input is
via Term::ReadLine if that is available. If not, we do the best we can.
We also recognize 'bye' and 'exit' as commands, which terminate the
method. In addition, 'show' is recognized as a synonym for 'get', and
'get' (or 'show') without arguments is special-cased to list all
attribute names and their values. Attributes listed without a value have
the undefined value.
There are also a couple meta-commands, that in effect wrap other
commands. These are specified before the command, and can (depending on
the meta-command) have effect either right before the command is
executed, right after it is executed, or both. If more than one
meta-command is specified, the before-actions take place in the order
specified, and the after-actions in the reverse of the order specified.
The 'time' meta-command times the command, and writes the timing to
standard error before any output from the command is written.
The 'olist' meta-command turns TLE data into an observing list. This
only affects results with C<spacetrack-type> of C<'orbit'>. If the
content is affected, the C<spacetrack-type> will be changed to
C<'observing-list'>. This meta-command is experimental, and may change
function or be retracted. It is unsupported when applied to commands
that do not return TLE data.
For commands that produce output, we allow a sort of pseudo-redirection
of the output to a file, using the syntax ">filename" or ">>filename".
If the ">" is by itself the next argument is the filename. In addition,
we do pseudo-tilde expansion by replacing a leading tilde with the
contents of environment variable HOME. Redirection can occur anywhere
on the line. For example,
SpaceTrack> catalog special >special.txt
sends the "Special Interest Satellites" to file special.txt. Line
terminations in the file should be appropriate to your OS.
Redirections will not be recognized as such if quoted or escaped. That
is, both C<< >foo >> and C<< >'foo' >> (without the double quotes) are
redirections to file F<foo>, but both "C<< '>foo' >>" and C<< \>foo >>
are arguments whose value is C<< >foo >>.
This method can also be called as a subroutine - i.e. as
lib/Astro/SpaceTrack.pm view on Meta::CPAN
'spacetrack-type' => 'orbit',
'spacetrack-source' => 'spacetrack',
'spacetrack-interface' => 2,
);
}
return $rslt;
}
=for html <a name="spacetrack_query_v2"></a>
=item $resp = $st->spacetrack_query_v2( @path );
This method exposes the Space Track version 2 interface (a.k.a the REST
interface). It has nothing to do with the (probably badly-named)
C<spacetrack()> method.
The arguments are the arguments to the REST interface. These will be
URI-escaped, and a login will be performed if necessary. This method
returns an C<HTTP::Response> object containing the results of the
operation.
Except for the URI escaping of the arguments and the implicit login,
this method interfaces directly to Space Track. It is provided for those
who want a way to experiment with the REST interface, or who wish to do
something not covered by the higher-level methods.
For example, if you want the JSON version of the satellite box score
(rather than the tab-delimited version provided by the C<box_score()>
method) you will find the JSON in the response object of the following
call:
my $resp = $st->spacetrack_query_v2( qw{
basicspacedata query class boxscore
format json predicates all
} );
);
If this method is called directly from outside the C<Astro::SpaceTrack>
name space, pragmata will be added to the results based on the
arguments, as follows:
For C<< basicspacedata => 'modeldef' >>
Pragma: spacetrack-type = modeldef
Pragma: spacetrack-source = spacetrack
Pragma: spacetrack-interface = 2
For C<< basicspacedata => 'query' >> and C<< class => 'tle' >> or
C<'tle_latest'>,
Pragma: spacetrack-type = orbit
Pragma: spacetrack-source = spacetrack
Pragma: spacetrack-interface = 2
=cut
{
our $SPACETRACK_DELAY_SECONDS = $ENV{SPACETRACK_DELAY_SECONDS} || 3;
my $spacetrack_delay_until;
sub _spacetrack_delay {
my ( $self ) = @_;
$SPACETRACK_DELAY_SECONDS
or return;
$self->{dump_headers} & DUMP_DRY_RUN
and return;
if ( defined $spacetrack_delay_until ) {
my $now = _time();
$now < $spacetrack_delay_until
and _sleep( $spacetrack_delay_until - $now );
}
$spacetrack_delay_until = _time() + $SPACETRACK_DELAY_SECONDS;
return;
}
}
{
my %tle_class = map { $_ => 1 } qw{ tle tle_latest gp gp_history };
sub spacetrack_query_v2 {
my ( $self, @args ) = @_;
# Space Track has announced that beginning September 22 2014
# they will begin limiting queries to 20 per minute. But they
# seem to have jumped the gun, since I get failures August 19
# 2014 if I don't throttle. None of this applies, though, if
# we're not actually executing the query.
$self->_spacetrack_delay();
delete $self->{_pragmata};
# # Note that we need to add the comma to URI::Escape's RFC3986 list,
# # since Space Track does not decode it.
# my $url = join '/',
# $self->_make_space_track_base_url( 2 ),
# map {
# URI::Escape::uri_escape( $_, '^A-Za-z0-9.,_~:-' )
# } @args;
my $uri = URI->new( $self->_make_space_track_base_url( 2 ) );
$uri->path_segments( @args );
# $url eq $uri->as_string()
# or warn "'$url' ne '@{[ $uri->as_string() ]}'";
# $url = $uri->as_string();
if ( my $resp = $self->_dump_request(
args => \@args,
method => 'GET',
url => $uri,
version => 2,
) ) {
return $resp;
}
$self->_check_cookie_generic( 2 )
or do {
my $resp = $self->login();
$resp->is_success()
or return $resp;
};
## warn "Debug - $url/$cgi";
# my $resp = $self->_get_agent()->get( $url );
my $resp = $self->_get_agent()->get( $uri );
if ( $resp->is_success() ) {
if ( $self->{pretty} &&
_find_rest_arg_value( \@args, format => 'json' ) eq 'json'
) {
my $json = $self->_get_json_object();
$resp->content( $json->encode( $json->decode(
$resp->content() ) ) );
}
if ( __PACKAGE__ ne caller ) {
my $kind = _find_rest_arg_value( \@args,
basicspacedata => '' );
my $class = _find_rest_arg_value( \@args,
class => '' );
if ( 'modeldef' eq $kind ) {
$self->_add_pragmata( $resp,
'spacetrack-type' => 'modeldef',
'spacetrack-source' => 'spacetrack',
'spacetrack-interface' => 2,
);
} elsif ( 'query' eq $kind && $tle_class{$class} ) {
$self->_add_pragmata( $resp,
'spacetrack-type' => 'orbit',
'spacetrack-source' => 'spacetrack',
'spacetrack-interface' => 2,
);
}
}
}
$self->__dump_response( $resp );
return $resp;
}
}
sub _find_rest_arg_value {
my ( $args, $name, $default ) = @_;
for ( my $inx = $#$args - 1; $inx >= 0; $inx -= 2 ) {
$args->[$inx] eq $name
and return $args->[$inx + 1];
}
return $default;
}
=for html <a name="update"></a>
=item $resp = $st->update( $file_name );
This method updates the named TLE file, which must be in JSON format. On
a successful update, the content of the returned HTTP::Response object
is the updated TLE data, in whatever format is desired. If any updates
were in fact found, the file is rewritten. The rewritten JSON will be
pretty if the C<pretty> attribute is true.
The file to be updated can be generated by using the C<-json> option on
any of the methods that accesses Space Track data. For example,
# Assuming $ENV{SPACETRACK_USER} contains
# username/password
my $st = Astro::SpaceTrack->new(
pretty => 1,
);
my $rslt = $st->spacetrack( { json => 1 }, 'iridium' );
$rslt->is_success()
or die $rslt->status_line();
open my $fh, '>', 'iridium.json'
or die "Failed to open file: $!";
print { $fh } $rslt->content();
close $fh;
The following is the equivalent example using the F<SpaceTrack> script:
SpaceTrack> set pretty 1
SpaceTrack> spacetrack -json iridium >iridium.json
This method reads the file to be updated, determines the highest C<FILE>
value, and then requests the given OIDs, restricting the return to
C<FILE> values greater than the highest found. If anything is returned,
the file is rewritten.
The following options may be specified:
-json
specifies the TLE be returned in JSON format
Options may be specified either in command-line style (that is, as
C<< spacetrack( '-json', ... ) >>) or as a hash reference (that is, as
C<< spacetrack( { json => 1 }, ... ) >>).
B<Note> that there is no way to specify the C<-rcs> or C<-effective>
options. If the file being updated contains these values, they will be
lost as the individual OIDs are updated.
=cut
{
my %encode = (
'3le' => sub {
my ( undef, $data ) = @_; # JSON object unused
return join '', map {
"$_->{OBJECT_NAME}\n$_->{TLE_LINE1}\n$_->{TLE_LINE2}\n"
} @{ $data };
},
json => sub {
my ( $json, $data ) = @_;
return $json->encode( $data );
},
tle => sub {
my ( undef, $data ) = @_; # JSON object unused
return join '', map {
"$_->{TLE_LINE1}\n$_->{TLE_LINE2}\n"
} @{ $data };
},
);
# Called dynamically
sub _update_opts { ## no critic (Subroutines::ProhibitUnusedPrivateSubroutines)
lib/Astro/SpaceTrack.pm view on Meta::CPAN
$go->getoptionsfromarray(
$args,
map {; "$_!" => sub {
$_[1] and do {
my $method = "DUMP_\U$_[0]";
$value |= $self->$method();
};
return;
}
} @dump_options
);
push @{ $args }, $value; # Since caller pops it.
} else {
$value =~ m/ \A 0 (?: [0-7]+ | x [[:xdigit:]]+ ) \z /smx
and $value = oct $value;
}
return ( $self->{$name} = $value );
}
{
my %id_file_name = (
MSWin32 => sub {
my $home = $ENV{HOME} || $ENV{USERPROFILE} || join '',
$ENV{HOMEDRIVE}, $ENV{HOMEPATH};
return "$home\\spacetrack.id";
},
VMS => sub {
my $home = $ENV{HOME} || 'sys$login';
return "$home:spacetrack.id";
},
);
sub __identity_file_name {
my $id_file = ( $id_file_name{$^O} || sub {
return join '/', $ENV{HOME}, '.spacetrack-identity' }
)->();
my $gpg_file = "$id_file.gpg";
-e $gpg_file
and return $gpg_file;
return $id_file;
}
}
# This basically duplicates the logic in Config::Identity
sub __identity_file_is_encrypted {
my $fn = __identity_file_name();
-B $fn
and return 1;
open my $fh, '<:encoding(utf-8)', $fn
or return;
local $/ = undef;
my $content = <$fh>;
close $fh;
return $content =~ m/ \Q----BEGIN PGP MESSAGE----\E /smx;
}
sub _mutate_identity {
my ( $self, $name, $value ) = @_;
defined $value
or $value = $ENV{SPACETRACK_IDENTITY};
if ( $value and my $identity = __spacetrack_identity() ) {
$self->set( %{ $identity } );
}
return ( $self->{$name} = $value );
}
=for html <a name="flush_identity_cache"></a>
=item Astro::SpaceTrack->flush_identity_cache();
The identity file is normally read only once, and the data cached. This
static method flushes the cache to force the identity data to be reread.
=cut
{
my $identity;
my $loaded;
sub flush_identity_cache {
$identity = $loaded = undef;
return;
}
sub __spacetrack_identity {
$loaded
and return $identity;
$loaded = 1;
my $fn = __identity_file_name();
-f $fn
or return $identity;
{
local $@ = undef;
eval {
require Config::Identity;
$identity = { Config::Identity->load( $fn ) };
1;
} or return;
}
foreach my $key ( qw{ username password } ) {
exists $identity->{$key}
or Carp::croak "Identity file omits $key";
}
scalar keys %{ $identity } > 2
and Carp::croak 'Identity file defines keys besides username and password';
return $identity;
}
}
{
my %need_logout = map { $_ => 1 } qw{ domain_space_track };
sub _mutate_spacetrack_interface {
my ( $self, $name, $value ) = @_;
my $version = $self->{space_track_version};
my $spacetrack_interface_info =
$self->{_space_track_interface}[$version];
exists $spacetrack_interface_info->{$name}
lib/Astro/SpaceTrack.pm view on Meta::CPAN
my $info = $catalogs{$source}
or Carp::confess "Bug - No such source as '$source'";
if ( ARRAY_REF eq ref $info ) {
my $inx = shift @args;
$info = $info->[$inx]
or Carp::confess "Bug - Illegal index $inx ",
"for '$source'";
}
my ( $catalog, $note ) = @args;
my $name = $no_such_name{$source} || $source;
my $lead = defined $catalog ?
$info->{$catalog} ?
"$name '$catalog' missing" :
"$name '$catalog' not found" :
"$name item not defined";
$lead .= defined $note ? " ($note)." : '.';
return HTTP::Response->new (HTTP_NOT_FOUND, "$lead\n")
unless $self->{verbose};
my $resp = $self->names ($source);
return HTTP::Response->new (HTTP_NOT_FOUND,
join '', "$lead Try one of:\n", $resp->content,
);
}
}
# _parse_args parses options off an argument list. The first
# argument must be a list reference of options to be parsed.
# This list is pairs of values, the first being the Getopt::Long
# specification for the option, and the second being a description
# of the option suitable for help text. Subsequent arguments are
# the arguments list to be parsed. It returns a reference to a
# hash containing the options, followed by any remaining
# non-option arguments. If the first argument after the list
# reference is a hash reference, it simply returns.
{
my $go = Getopt::Long::Parser->new();
sub _parse_args {
my ( $lgl_opts, @args ) = @_;
unless ( ARRAY_REF eq ref $lgl_opts ) {
unshift @args, $lgl_opts;
( my $caller = ( caller 1 )[3] ) =~ s/ ( .* ) :: //smx;
my $pkg = $1;
my $code = $pkg->can( "_${caller}_opts" )
or Carp::confess "Bug - _${caller}_opts not found";
$lgl_opts = $code->();
}
my $opt;
if ( HASH_REF eq ref $args[0] ) {
$opt = { %{ shift @args } }; # Poor man's clone.
# Validation is new, so I insert a hack to turn it off if need
# be.
unless ( $ENV{SPACETRACK_SKIP_OPTION_HASH_VALIDATION} ) {
my %lgl = _extract_keys( $lgl_opts );
my @bad;
foreach my $key ( keys %{ $opt } ) {
$lgl{$key}
or push @bad, $key;
}
@bad
and _parse_args_failure(
carp => 1,
name => \@bad,
legal => { @{ $lgl_opts } },
suffix => <<'EOD',
You cam suppress this warning by setting environment variable
SPACETRACK_SKIP_OPTION_HASH_VALIDATION to a value Perl understands as
true (say, like 1), but this should be considered a stopgap while you
fix the calling code, or have it fixed, since my plan is to make this
fatal.
EOD
);
}
} else {
$opt = {};
my %lgl = @{ $lgl_opts };
$go->getoptionsfromarray(
\@args,
$opt,
keys %lgl,
)
or _parse_args_failure( legal => \%lgl );
}
return ( $opt, @args );
}
}
sub _parse_args_failure {
my %arg = @_;
my $msg = $arg{carp} ? 'Warning - ' : 'Error - ';
if ( defined $arg{name} ) {
my @names = ( ARRAY_REF eq ref $arg{name} ) ?
@{ $arg{name} } :
$arg{name};
@names
or return;
my $opt = @names > 1 ? 'Options' : 'Option';
my $txt = join ', ', map { "-$_" } sort @names;
$msg .= "$opt $txt illegal.\n";
}
if ( defined $arg{legal} ) {
$msg .= "Legal options are\n";
foreach my $opt ( sort keys %{ $arg{legal} } ) {
my $desc = $arg{legal}{$opt};
$opt = _extract_keys( $opt );
$msg .= " -$opt - $desc\n";
}
$msg .= <<"EOD";
with dates being either Perl times, or numeric year-month-day, with any
non-numeric character valid as punctuation.
EOD
}
defined $arg{suffix}
and $msg .= $arg{suffix};
$arg{carp}
or Carp::croak $msg;
Carp::carp $msg;
return;
}
# Parse an international launch ID in the form yyyy-sssp or yysssp.
# In the yyyy-sssp form, the year can be two digits (in which case 57-99
# are 1957-1999 and 00-56 are 2000-2056) and the dash can be any
# non-alpha, non-digit, non-space character. In either case, trailing
# fields are optional. If provided, the part ('p') can be multiple
# alphabetic characters. Only fields actually specified will be
# returned.
lib/Astro/SpaceTrack.pm view on Meta::CPAN
the banner text on invocation.
The default is true (i.e. 1).
=item cookie_expires (number)
This attribute specifies the expiration time of the cookie. You should
only set this attribute with a previously-retrieved value, which
matches the cookie.
=item cookie_name (string)
This attribute specifies the name of the session cookie. You should not
need to change this in normal circumstances, but if Space Track changes
the name of the session cookie you can use this to get you going again.
=item domain_space_track (string)
This attribute specifies the domain name of the Space Track web site.
The user will not normally need to modify this, but if the web site
changes names for some reason, this attribute may provide a way to get
queries going again.
The default is C<'www.space-track.org'>. This will change if necessary
to remain appropriate to the Space Track web site.
=item fallback (Boolean)
This attribute specifies that orbital elements should be fetched from
the redistributer if the original source is offline. At the moment the
only method affected by this is celestrak().
The default is false (i.e. 0).
=item filter (Boolean)
If true, this attribute specifies that the shell is being run in filter
mode, and prevents any output to STDOUT except orbital elements -- that
is, if I found all the places that needed modification.
The default is false (i.e. 0).
=item identity (Boolean)
If this attribute is set to a true value, the C<Astro::SpaceTrack>
object will attempt to load attributes from an identity file. This will
only do anything if the identity file exists and
L<Config::Identity|Config::Identity> is installed. In addition, if the
identity file is encrypted C<gpg2> must be installed and properly
configured. See L<IDENTITY FILE|/IDENTITY FILE> below for details of the
identity file.
I have found that C<gpg> does not seem to work nicely, even though
L<Config::Identity|Config::Identity> prefers it to C<gpg2> if both are
present. The L<Config::Identity|Config::Identity> documentation says
that you can override this by setting environment variable C<CI_GPG>
to the executable you want used.
If this attribute is unspecified (to C<new()> or specified as C<undef>
(to C<new()> or C<set()>), the value of environment variable
C<SPACETRACK_IDENTITY> will be used as the new value.
When a new object is instantiated, the identity is processed first; in
this way attribute values that come from the environment or are
specified explicitly override those that come from the identity file. If
you explicitly set this on an already-instantiated object, the attribute
values from the identity file will replace those in the object.
When you instantiate an object, the identity from environment variable
C<SPACETRACK_USER> will be preferred over the value from the identity
file, if any, even if the C<identity> attribute is explicitly set true.
=item iridium_status_format (string)
This attribute specifies the default format of the data returned by the
C<iridium_status()> method. Valid values are 'kelso', 'sladen' or
'spacetrack'. See that method for more information.
As of version 0.100_02, the default is C<'kelso'>. It used to be
C<'mccants'>, but Mike McCants no longer maintains his Iridium status
web page, and format C<'mccants'> was removed as of version 0.137.
This attribute is B<deprecated>.
=item max_range (number)
This attribute specifies the maximum size of a range of NORAD IDs to be
retrieved. Its purpose is to impose a sanity check on the use of the
range functionality.
The default is 500.
=item password (text)
This attribute specifies the Space-Track password.
The default is an empty string.
=item pretty (Boolean)
This attribute specifies whether the content of the returned
L<HTTP::Response|HTTP::Response> is to be pretty-formatted. Currently
this only applies to Space Track data returned in C<JSON> format.
Pretty-formatting the C<JSON> is extra overhead, so unless you intend to
read the C<JSON> yourself this should probably be false.
The default is C<0> (i.e. false).
=item prompt (string)
This attribute specifies the prompt issued by the C<shell()> method. The
default is C<< 'SpaceTrack> ' >>.
=item scheme_space_track (string)
This attribute specifies the URL scheme used to access the Space Track
web site. The user will not normally need to modify this, but if the web
site changes schemes for some reason, this attribute may provide a way
to get queries going again.
The default is C<'https'>.
=item session_cookie (text)
This attribute specifies the session cookie. You should only set it
with a previously-retrieved value.
The default is an empty string.
=item space_track_version (integer)
lib/Astro/SpaceTrack.pm view on Meta::CPAN
his web site.
The default is 'https://celestrak.org/SpaceTrack/query/iridium.txt'
This attribute is B<deprecated>.
=item url_iridium_status_mccants (text)
This attribute is B<deprecated>, and any access of it will be fatal.
=item url_iridium_status_sladen (text)
This attribute specifies the location of Rod Sladen's Iridium
Constellation Status page. You should normally not need to change this,
but it is provided so you will not be dead in the water if Mr. Sladen
needs to change his ISP or re-arrange his web site.
The default is 'http://www.rod.sladen.org.uk/iridium.htm'.
This attribute is B<deprecated>.
=item username (text)
This attribute specifies the Space-Track username.
The default is an empty string.
=item verbose (Boolean)
This attribute specifies verbose error messages.
The default is false (i.e. 0).
=item verify_hostname (Boolean)
This attribute specifies whether C<https:> certificates are verified.
If you set this false, you can not verify that hosts using C<https:> are
who they say they are, but it also lets you work around invalid
certificates. Currently only the Space Track web site uses C<https:>.
B<Note> that the default has changed, as follows:
* In version 0.060_08 and earlier, the default was true, to mimic
earlier behavior.
* In version 0.060_09 this was changed to false, in the belief that the
code should work out of the box (which it did not when verify_hostname
was true, at least as of mid-July 2012).
* On September 30 2012 Space Track announced that they had their SSL
certificates set up, so in 0.064_01 the default became false again.
* On August 19 2014 Perl's SSL logic stopped accepting Mike McCants'
GoDaddy certificate, so starting with version 0.086_02 the default is
false once again.
* On December 11 2014 I noticed that Perl was accepting Mike McCants'
certificate again, so starting with version 0.088_01 the default
is restored to true.
If environment variable C<SPACETRACK_VERIFY_HOSTNAME> is defined, its
value will be used as the default of this attribute. Otherwise the
default is false (i.e. 0).
=item webcmd (string)
This attribute specifies a system command that can be used to launch
a URL into a browser. If specified, the 'help' command will append
a space and the metacpan.org URL for the documentation for this
version of Astro::SpaceTrack, and spawn that command to the operating
system. You can use 'open' under Mac OS X, and 'start' under Windows.
Anyone else will probably need to name an actual browser.
As of version 0.105_01, a value of C<'1'> causes
L<Browser::Open|Browser::Open> to be loaded, and the web command is
taken from it. All other true values are deprecated, on the following
schedule:
=over
=item 2018-11-01: First use of deprecated value will warn;
=item 2019-05-01: All uses of deprecated value will warn;
=item 2019-11-01: Any use of deprecated value is fatal;
=item 2020-05-01: Attribute is treated as Boolean.
=back
The above schedule may be extended based on what other changes are
needed, but will not be compressed.
The default is C<undef>, which leaves the functionality disabled.
=item with_name (Boolean)
This attribute specifies whether the returned element sets should
include the common name of the body (three-line format) or not
(two-line format). This attribute may be ignored; see the individual
method for details.
The default is false (i.e. 0).
=back
=head1 IDENTITY FILE
This is a L<Config::Identity|Config::Identity> file which specifies the
username and password values for the user. This file is stored in the
user's home directory, and is F<spacetrack.id> under C<MSWin32> or
C<VMS>, or F<.spacetrack-identity> under any other operating system.
If desired, the file can be encrypted using GPG; in this case, to be
useful, C<gpg> and C<gpg-agent> must be installed and properly
configured. Because of implementation details in
L<Config::Identity|Config::Identity>, you may need to either ensure that
C<gpg> is not in your C<PATH>, or set the C<CI_GPG> environment variable
to the path to C<gpg2>. The encrypted file can optionally have C<.gpg>
appended to its name for the convenience of users of the vim-gnupg
plugin and similar software. If the identity file exists both with and
without the C<.gpg> suffix, the suffixed version will be used.
Note that this file is normally read only once during the life of the
Perl process, and the result cached. The username and password that are
set when C<identity> becomes true come from the cache. If you want a
running script to see new identity file information you must call static
method C<flush_identity_cache()>.
=head1 GLOBALS
The following globals modify the behaviour of this class. If you modify
their values, your modifications should be properly localized. For
example:
{
local $SPACETRACK_DELAY_SECONDS = 42;
$rslt = $st->search_name( 'iss' );
}
=head2 $SPACETRACK_DELAY_SECONDS
This global holds the delay in seconds between queries. It defaults to 3
(or the value of environment variable C<SPACETRACK_DELAY_SECONDS> if
that is true), and should probably not be messed with. But if Space
Track is being persnickety about timing you can set it to a larger
number. This variable must be set to a number. If
L<Time::HiRes|Time::HiRes> is not available this number must be an
integer.
This global is not exported. You must refer to it as
C<$Astro::SpaceTrack::SPACETRACK_DELAY_SECONDS>.
=head1 ENVIRONMENT
The following environment variables are recognized by Astro::SpaceTrack.
=head2 SPACETRACK_DELAY_SECONDS
This environment variable should be set to a positive number to change
the default delay between Space Track queries. This is C<not> something
you should normally need to do. If L<Time::HiRes|Time::HiRes> is not
available this number must be an integer.
This environment variable is only used to initialize
C<$SPACETRACK_DELAY_SECONDS>. If you wish to change the delay you must
assign to the global.
=head2 SPACETRACK_IDENTITY
This environment variable specifies the default value for the identity
attribute any time an undefined value for that attribute is specified.
=head2 SPACETRACK_OPT
If environment variable SPACETRACK_OPT is defined at the time an
Astro::SpaceTrack object is instantiated, it is broken on spaces,
and the result passed to the set command.
If you specify username or password in SPACETRACK_OPT and you also
specify SPACETRACK_USER, the latter takes precedence, and arguments
passed explicitly to the new () method take precedence over both.
=head2 SPACETRACK_TEST_LIVE
If environment variable C<SPACETRACK_TEST_LIVE> is defined to a true
value (in the Perl sense), tests that use the Space Track web site will
actually access it. Otherwise they will either use canned data (i.e. a
regression test) or be skipped.
=head2 SPACETRACK_USER
If environment variable SPACETRACK_USER is defined at the time an
Astro::SpaceTrack object is instantiated, the username and password will
be initialized from it. The value of the environment variable should be
the username and password, separated by either a slash (C<'/'>) or a
colon (C<':'>). That is, either C<'yehudi/menuhin'> or
C<'yehudi:menuhin'> are accepted.
An explicit username and/or password passed to the new () method
overrides the environment variable, as does any subsequently-set
username or password.
=head2 SPACETRACK_VERIFY_HOSTNAME
As of version 0.086_02, if environment variable
C<SPACETRACK_VERIFY_HOSTNAME> is defined at the time an
C<Astro::SpaceTrack> object is instantiated, its value will be used for
the default value of the C<verify_hostname> attribute.
=head2 SPACETRACK_SKIP_OPTION_HASH_VALIDATION
As of version 0.081_01, method options passed as a hash reference will
be validated. Before this, only command-line-style options were
validated. If the validation causes problem, set this environment
variable to a value Perl sees as true (i.e. anything but C<0> or C<''>)
to revert to the old behavior.
Support for this environment variable will be put through a deprecation
cycle and removed once the validation code is deemed solid.
=head1 EXECUTABLES
A couple specimen executables are included in this distribution:
=head2 SpaceTrack
This is just a wrapper for the shell () method.
=head2 SpaceTrackTk
This provides a Perl/Tk interface to Astro::SpaceTrack.
=head1 BUGS
This software is essentially a web page scraper, and relies on the
stability of the user interface to Space Track. The Celestrak
portion of the functionality relies on the presence of .txt files
named after the desired data set residing in the expected location.
The Human Space Flight portion of the functionality relies on the
stability of the layout of the relevant web pages.
This software has not been tested under a HUGE number of operating
systems, Perl versions, and Perl module versions. It is rather likely,
for example, that the module will die horribly if run with an
insufficiently-up-to-date version of LWP.
Support is by the author. Please file bug reports at
L<https://rt.cpan.org/Public/Dist/Display.html?Name=Astro-SpaceTrack>,
L<https://github.com/trwyant/perl-Astro-SpaceTrack/issues/>, or in
electronic mail to the author.
=head1 MODIFICATIONS OF HISTORICAL INTEREST
=head2 Data Throttling
Space Track announced August 19 2013 that beginning September 22 they
would limit users to less than 20 API queries per minute. Experience
seems to say they jumped the gun - at least, server errors during
testing were turned into success by throttling queries to one every
three seconds.
The throttling functionality will make use of L<Time::HiRes|Time::HiRes>
if it is available; otherwise it will simply use the built-in C<sleep()>
and C<time()>, with consequent loss of precision.
Unfortunately this makes testing slower. Sorry.
=head2 Quantitative RCS Data
On July 21 2014 Space Track announced the plan to remove quantitative
( run in 0.528 second using v1.01-cache-2.11-cpan-75ffa21a3d4 )