File Coverage

blib/lib/Weather/WeatherKit.pm
Criterion Covered Total %
statement 14 77 18.1
branch 0 26 0.0
condition 0 36 0.0
subroutine 5 12 41.6
pod 4 4 100.0
total 23 155 14.8


line stmt bran cond sub pod time code
1             package Weather::WeatherKit;
2              
3 1     1   107487 use 5.008;
  1         4  
4 1     1   7 use strict;
  1         2  
  1         32  
5 1     1   6 use warnings;
  1         2  
  1         25  
6              
7 1     1   7 use Carp;
  1         3  
  1         86  
8 1     1   778 use Crypt::JWT qw(encode_jwt);
  1         59070  
  1         1061  
9              
10             =head1 NAME
11              
12             Weather::WeatherKit - Apple WeatherKit REST API client
13              
14             =head1 VERSION
15              
16             Version 0.01
17              
18             =cut
19              
20             our $VERSION = '0.0_1';
21              
22             =head1 SYNOPSIS
23              
24             use Weather::WeatherKit;
25              
26             my $wk = Weather::WeatherKit->new(
27             team_id => $apple_team_id, # Your 10-char Developer Team Id
28             service_id => $weatherkit_service_id, # The WeatherKit Service Id
29             key_id => $key_id, # ID of the WeatherKit developer key
30             key => $private_key # Encrypted private key (PEM)
31             );
32            
33             my $report = $wk->get(
34             lat => $lat, # Latitude
35             lon => $lon, # Longitude
36             dataSets => $datasets # e.g. currentWeather - comma-separated list
37             );
38              
39             =head1 DESCRIPTION
40              
41             Weather::WeatherKit provides basic access to the Apple WeatherKit REST API (v1).
42             WeatherKit replaces the Dark Sky API and requires an Apple developer subscription.
43              
44             Pease see API documentation: L
45             for dataSets and usage options as well as the required attribution.
46              
47             It was made to serve the apps L and
48             L, but if your service
49             requires some extra functionality, feel free to contact the author about it.
50              
51             =head1 CONSTRUCTOR METHOD
52              
53             =head2 C
54              
55             my $wk = Weather::WeatherKit->new(
56             team_id => "MLU84X58U4",
57             service_id => "com.domain.myweatherapp",
58             key_id => $key_id,
59             key => $private_key?,
60             key_file => $private_key_pem?,
61             language => $lang_code?,
62             timeout => $timeout_sec?,
63             expiration => $expire_secs?,
64             ua => $lwp_ua?,
65             curl => $use_curl?
66             );
67            
68             Required parameters:
69              
70             =over 4
71              
72             =item * C : Your 10-character Apple developer Team Id - it can be located
73             on the Apple developer portal.
74              
75             =item * C : The WeatherKit Service Identifier created on the Apple
76             developer portal. Usually a reverse-domain type string is used for this.
77              
78             =item * C : The ID of the WeatherKit key created on the Apple developer portal.
79              
80             =item * C : The encrypted WeatherKit private key file that you created on
81             the Apple developer portal. On the portal you download a PKCS8 format file (.p8),
82             which you first need to convert to the PEM format. On a Mac you can convert it simply:
83              
84             openssl pkcs8 -nocrypt -in AuthKey_.p8 -out AuthKey_.pem
85              
86             =item * C : Instead of the C<.pem> file, you can pass its contents directly
87             as a string.
88              
89             =back
90              
91             Optional parameters:
92              
93             =over 4
94              
95             =item * C : Language code. Default: C.
96              
97             =item * C : Timeout for requests in secs. Default: C<30>.
98              
99             =item * C : Pass your own L to customise the agent string etc.
100              
101             =item * C : If true, fall back to using the C command line program.
102             This is useful if you have issues adding http support to L, which
103             is the default method for the WeatherKit requests.
104              
105             =item * C : Token expiration time in seconds. Tokens are cached until
106             there are less than 10 minutes left to expiration. Default: C<7200>.
107              
108             =back
109              
110             =head1 METHODS
111              
112             =head2 C
113              
114             my $report = $wk->get(
115             lat => $lat,
116             lon => $lon,
117             dataSets => $datasets
118             %args?
119             );
120              
121             my %report = $wk->get( ... );
122              
123             Fetches datasets (weather report, forecast, alert...) for the requested location.
124             Returns a string containing the JSON data, except in array context, in which case,
125             as a convenience, it will use L to decode it directly to a Perl hash.
126              
127             If the request is not successful, it will C throwing the C<< HTTP::Response->status_line >>.
128              
129             =over 4
130            
131             =item * C : Latitude (-90 to 90).
132              
133             =item * C : Longitude (-18 to 180).
134              
135             =item * C : A comma-separated string of the dataset(s) you request. Example
136             supported data sets: C.
137             Some data sets might not be available for all locations. Will return empty results
138             if parameter is missing.
139              
140             =item * C<%args> : See the official API documentation for the supported weather API
141             query parameters which you can can pass as key/value pairs.
142              
143             =back
144              
145             =head2 C
146              
147             my $response = $wk->get_response(
148             lat => $lat,
149             lon => $lon,
150             dataSets => $datasets
151             %args?
152             );
153              
154             Same as C except it returns the full L from the API (so you
155             can handle bad requests yourself).
156              
157             =head1 CONVENIENCE METHODS
158              
159             =head2 C
160              
161             my $jwt = $wk->jwt(
162             iat => $iat?,
163             exp => $exp?
164             );
165              
166             Returns the JSON Web Token string in case you need it. Will return a cached one
167             if it has more than 10 minutes until expiration and you don't explicitly pass an
168             C argument.
169              
170             =over 4
171            
172             =item * C : Specify the token creation timestamp. Default is C.
173              
174             =item * C : Specify the token expiration timestamp. Passing this parameter
175             will force the creation of a new token. Default is C (or what you
176             specified in the constructor).
177              
178             =back
179              
180             =cut
181              
182             sub new {
183 0     0 1   my $class = shift;
184              
185 0           my $self = {};
186 0           bless($self, $class);
187              
188 0           my %args = @_;
189              
190 0 0 0       croak("10 digit team_id expected.") unless $args{team_id} && length($args{team_id}) == 10;
191 0           $self->{team_id} = $args{team_id};
192              
193             ($self->{$_} = $args{$_} || croak("$_ required."))
194 0   0       foreach qw/service_id key_id/;
195              
196 0 0         unless ($args{key}) {
197 0 0         croak("key or key_file required.") unless $args{key_file};
198 0 0         open my $fh, '<', $args{key_file} or die "Can't open file $!";
199 0           $args{key} = do { local $/; <$fh> };
  0            
  0            
200             }
201 0           $self->{key} = \$args{key};
202 0   0       $self->{language} = $args{language} || "en_US";
203 0   0       $self->{timeout} = $args{timeout} || 30;
204 0   0       $self->{expiration} = $args{expiration} || 7200;
205 0           $self->{ua} = $args{ua};
206 0           $self->{curl} = $args{curl};
207              
208 0           return $self;
209             }
210              
211             sub get {
212 0     0 1   my $self = shift;
213 0           my %args = @_;
214              
215 0           my $resp = $self->get_response(%args);
216              
217 0 0         return _output($resp, wantarray) if $self->{curl};
218              
219 0 0         if ($resp->is_success) {
220 0           return _output($resp->decoded_content, wantarray);
221             }
222             else {
223 0           die $resp->status_line;
224             }
225             }
226              
227             sub get_response {
228 0     0 1   my $self = shift;
229 0           my %args = @_;
230 0   0       $args{language} ||= $self->{language};
231              
232             croak("lat between -90 and 90 expected")
233 0 0 0       unless defined $args{lat} && abs($args{lat}) <= 90;
234              
235             croak("lon between -180 and 180 expected")
236 0 0 0       unless defined $args{lon} && abs($args{lon}) <= 180;
237              
238 0           my $url = _weather_url(%args);
239 0           my $jwt = $self->jwt;
240              
241 0 0 0       unless ($self->{curl} || $self->{ua}) {
242 0           require LWP::UserAgent;
243             $self->{ua} = LWP::UserAgent->new(
244             agent => "libwww-perl Weather::WeatherKit/$VERSION",
245             timeout => $self->{timeout}
246 0           );
247             }
248              
249 0           return _fetch($self->{ua}, $url, $jwt);
250             }
251              
252             sub jwt {
253 0     0 1   my $self = shift;
254 0           my %args = @_;
255              
256             # Return cached one
257             return $self->{jwt}
258 0 0 0       if !$args{exp} && $self->{jwt_exp} && $self->{jwt_exp} >= time() + 600;
      0        
259              
260 0   0       $args{iat} ||= time();
261 0   0       $self->{jwt_exp} = $args{exp} || (time() + $self->{expiration});
262              
263             my $data = {
264             iss => $self->{team_id},
265             sub => $self->{service_id},
266             exp => $self->{jwt_exp},
267             iat => $args{iat}
268 0           };
269              
270             $self->{jwt} = encode_jwt(
271             payload => $data,
272             alg => 'ES256',
273             key => $self->{key},
274             extra_headers => {
275             kid => $self->{key_id},
276 0           id => "$self->{team_id}.$self->{service_id}",
277             typ => "JWT"
278             }
279             );
280              
281 0           return $self->{jwt};
282             }
283              
284             sub _fetch {
285 0     0     my ($ua, $url, $jwt) = @_;
286              
287             return
288 0 0         `curl "$url" -A "Curl Weather::WeatherKit/$VERSION" -s -H 'Authorization: Bearer $jwt'`
289             unless $ua;
290              
291 0           return $ua->get($url, Authorization => "Bearer $jwt");
292             }
293              
294             sub _weather_url {
295 0     0     my %args = @_;
296 0           my $url =
297             "https://weatherkit.apple.com/api/v1/weather/{language}/{lat}/{lon}";
298              
299 0           $url =~ s/{$_}/delete $args{$_}/e foreach qw/language lat lon/;
  0            
300              
301 0           my $params = join("&", map {"$_=$args{$_}"} keys %args);
  0            
302              
303 0 0         $url .= "?$params" if $params;
304              
305 0           return $url;
306             }
307              
308             sub _output {
309 0     0     my $str = shift;
310 0           my $json = shift;
311              
312 0 0         return $str unless $json;
313              
314 0           require JSON;
315 0           return %{JSON::decode_json($str)};
  0            
316             }
317              
318             =head1 AUTHOR
319              
320             Dimitrios Kechagias, C<< >>
321              
322             =head1 BUGS
323              
324             Please report any bugs or feature requests either on GitHub, or on RT (via the email
325             C or web interface at L).
326              
327             I will be notified, and then you'll automatically be notified of progress on your bug as I make changes.
328              
329             =head1 GIT
330              
331             L
332              
333             =head1 LICENSE AND COPYRIGHT
334              
335             This software is copyright (c) 2023 by Dimitrios Kechagias.
336              
337             This is free software; you can redistribute it and/or modify it under
338             the same terms as the Perl 5 programming language system itself.
339              
340             =cut
341              
342             1;