File Coverage

blib/lib/Weather/WeatherKit.pm
Criterion Covered Total %
statement 77 77 100.0
branch 24 26 92.3
condition 30 36 83.3
subroutine 12 12 100.0
pod 4 4 100.0
total 147 155 94.8


line stmt bran cond sub pod time code
1             package Weather::WeatherKit;
2              
3 3     3   662726 use 5.008;
  3         29  
4 3     3   17 use strict;
  3         12  
  3         69  
5 3     3   15 use warnings;
  3         10  
  3         67  
6              
7 3     3   14 use Carp;
  3         6  
  3         191  
8 3     3   1909 use Crypt::JWT qw(encode_jwt);
  3         166108  
  3         3051  
9              
10             =head1 NAME
11              
12             Weather::WeatherKit - Apple WeatherKit REST API client
13              
14             =head1 VERSION
15              
16             Version 0.11
17              
18             =cut
19              
20             our $VERSION = '0.11';
21              
22             =head1 SYNOPSIS
23              
24             use Weather::WeatherKit;
25              
26             my $wk = Weather::WeatherKit->new(
27             team_id => $apple_team_id, # Apple Developer Team Id
28             service_id => $weatherkit_service_id, # WeatherKit Service Id
29             key_id => $key_id, # WeatherKit developer key ID
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 the L
45             for datasets and usage options as well as the L.
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
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             Requires L, unless the C option was set.
128              
129             If the request is not successful, it will C throwing the C<< HTTP::Response->status_line >>.
130              
131             =over 4
132            
133             =item * C : Latitude (-90 to 90).
134              
135             =item * C : Longitude (-18 to 180).
136              
137             =item * C : A comma-separated string of the dataset(s) you request. Example
138             supported data sets: C.
139             Some data sets might not be available for all locations. Will return empty results
140             if parameter is missing.
141              
142             =item * C<%args> : See the official API documentation for the supported weather API
143             query parameters which you can pass as key/value pairs.
144              
145             =back
146              
147             =head2 C
148              
149             my $response = $wk->get_response(
150             lat => $lat,
151             lon => $lon,
152             dataSets => $datasets
153             %args?
154             );
155              
156             Same as C except it returns the full L from the API (so you
157             can handle bad requests yourself).
158              
159             =head1 CONVENIENCE METHODS
160              
161             =head2 C
162              
163             my $jwt = $wk->jwt(
164             iat => $iat?,
165             exp => $exp?
166             );
167              
168             Returns the JSON Web Token string in case you need it. Will return a cached one
169             if it has more than 10 minutes until expiration and you don't explicitly pass an
170             C argument.
171              
172             =over 4
173            
174             =item * C : Specify the token creation timestamp. Default is C.
175              
176             =item * C : Specify the token expiration timestamp. Passing this parameter
177             will force the creation of a new token. Default is C (or what you
178             specified in the constructor).
179              
180             =back
181              
182             =cut
183              
184             sub new {
185 10     10 1 18478 my $class = shift;
186              
187 10         23 my $self = {};
188 10         23 bless($self, $class);
189              
190 10         37 my %args = @_;
191              
192 10 100 100     385 croak("10 digit team_id expected.") unless $args{team_id} && length($args{team_id}) == 10;
193 8         33 $self->{team_id} = $args{team_id};
194              
195             ($self->{$_} = $args{$_} || croak("$_ required."))
196 8   66     227 foreach qw/service_id key_id/;
197              
198 6 100       23 unless ($args{key}) {
199 3 100       128 croak("key or key_file required.") unless $args{key_file};
200 2 100       125 open my $fh, '<', $args{key_file} or die "Can't open file $!";
201 1         4 $args{key} = do { local $/; <$fh> };
  1         7  
  1         40  
202             }
203 4         22 $self->{key} = \$args{key};
204 4   100     25 $self->{language} = $args{language} || "en_US";
205 4   100     25 $self->{timeout} = $args{timeout} || 30;
206 4   100     37 $self->{expiration} = $args{expiration} || 7200;
207 4         17 $self->{ua} = $args{ua};
208 4         28 $self->{curl} = $args{curl};
209              
210 4         28 return $self;
211             }
212              
213             sub get {
214 8     8 1 7504 my $self = shift;
215 8         26 my %args = @_;
216              
217 8         32 my $resp = $self->get_response(%args);
218              
219 3 50       380 return _output($resp, wantarray) if $self->{curl};
220              
221 3 100       13 if ($resp->is_success) {
222 2         36 return _output($resp->decoded_content, wantarray);
223             }
224             else {
225 1         17 die $resp->status_line;
226             }
227             }
228              
229             sub get_response {
230 8     8 1 28 my $self = shift;
231 8         21 my %args = @_;
232 8   66     54 $args{language} ||= $self->{language};
233              
234             croak("lat between -90 and 90 expected")
235 8 100 100     230 unless defined $args{lat} && abs($args{lat}) <= 90;
236              
237             croak("lon between -180 and 180 expected")
238 6 100 100     175 unless defined $args{lon} && abs($args{lon}) <= 180;
239              
240 4         25 my $url = _weather_url(%args);
241 4         20 my $jwt = $self->jwt;
242              
243 3 100 66     26 unless ($self->{curl} || $self->{ua}) {
244 2         26 require LWP::UserAgent;
245             $self->{ua} = LWP::UserAgent->new(
246             agent => "libwww-perl Weather::WeatherKit/$VERSION",
247             timeout => $self->{timeout}
248 2         33 );
249             }
250              
251 3         6932 return _fetch($self->{ua}, $url, $jwt);
252             }
253              
254             sub jwt {
255 9     9 1 7421 my $self = shift;
256 9         21 my %args = @_;
257              
258             # Return cached one
259             return $self->{jwt}
260 9 100 100     78 if !$args{exp} && $self->{jwt_exp} && $self->{jwt_exp} >= time() + 600;
      66        
261              
262 5   66     30 $args{iat} ||= time();
263 5   66     21 $self->{jwt_exp} = $args{exp} || (time() + $self->{expiration});
264              
265             my $data = {
266             iss => $self->{team_id},
267             sub => $self->{service_id},
268             exp => $self->{jwt_exp},
269             iat => $args{iat}
270 5         47 };
271              
272             $self->{jwt} = encode_jwt(
273             payload => $data,
274             alg => 'ES256',
275             key => $self->{key},
276             extra_headers => {
277             kid => $self->{key_id},
278 5         43 id => "$self->{team_id}.$self->{service_id}",
279             typ => "JWT"
280             }
281             );
282              
283 4         36170 return $self->{jwt};
284             }
285              
286             sub _fetch {
287 3     3   15 my ($ua, $url, $jwt) = @_;
288              
289             return
290 3 50       17 `curl "$url" -A "Curl Weather::WeatherKit/$VERSION" -s -H 'Authorization: Bearer $jwt'`
291             unless $ua;
292              
293 3         20 return $ua->get($url, Authorization => "Bearer $jwt");
294             }
295              
296             sub _weather_url {
297 6     6   3062 my %args = @_;
298 6         12 my $url =
299             "https://weatherkit.apple.com/api/v1/weather/{language}/{lat}/{lon}";
300              
301 6         105 $url =~ s/{$_}/delete $args{$_}/e foreach qw/language lat lon/;
  18         152  
302              
303 6         21 my $params = join("&", map {"$_=$args{$_}"} keys %args);
  1         6  
304              
305 6 100       20 $url .= "?$params" if $params;
306              
307 6         32 return $url;
308             }
309              
310             sub _output {
311 2     2   460 my $str = shift;
312 2         6 my $json = shift;
313              
314 2 100       17 return $str unless $json;
315              
316 1         10 require JSON;
317 1         3 return %{JSON::decode_json($str)};
  1         19  
318             }
319              
320             =head1 AUTHOR
321              
322             Dimitrios Kechagias, C<< >>
323              
324             =head1 BUGS
325              
326             Please report any bugs or feature requests either on L (preferred), or on RT (via the email
327             C or L).
328              
329             I will be notified, and then you'll automatically be notified of progress on your bug as I make changes.
330              
331             =head1 GIT
332              
333             L
334              
335             =head1 LICENSE AND COPYRIGHT
336              
337             This software is copyright (c) 2023 by Dimitrios Kechagias.
338              
339             This is free software; you can redistribute it and/or modify it under
340             the same terms as the Perl 5 programming language system itself.
341              
342             =cut
343              
344             1;