File Coverage

blib/lib/Net/Google/Spreadsheets/V4.pm
Criterion Covered Total %
statement 65 129 50.3
branch 15 42 35.7
condition 2 19 10.5
subroutine 16 24 66.6
pod 4 7 57.1
total 102 221 46.1


line stmt bran cond sub pod time code
1             package Net::Google::Spreadsheets::V4;
2              
3 3     3   207979 use strict;
  3         16  
  3         90  
4 3     3   17 use warnings;
  3         6  
  3         69  
5 3     3   72 use 5.010_000;
  3         12  
6 3     3   1941 use utf8;
  3         44  
  3         16  
7              
8             our $VERSION = '0.003';
9              
10             use Class::Accessor::Lite (
11 3         25 new => 0,
12             ro => [qw(ua csv spreadsheet_id endpoint)],
13 3     3   1662 );
  3         3769  
14              
15 3     3   1861 use Data::Validator;
  3         95264  
  3         108  
16 3     3   1619 use Log::Minimal env_debug => 'NGS4_DEBUG';
  3         62659  
  3         25  
17 3     3   440 use Carp;
  3         10  
  3         173  
18 3     3   2431 use Net::Google::DataAPI::Auth::OAuth2;
  3         295769  
  3         136  
19 3     3   35 use Net::OAuth2::AccessToken;
  3         7  
  3         62  
20 3     3   2456 use Text::CSV;
  3         64840  
  3         127  
21 3     3   1473 use Furl;
  3         75907  
  3         99  
22 3     3   2043 use JSON;
  3         24889  
  3         20  
23 3     3   1714 use Sub::Retry;
  3         1399  
  3         3981  
24              
25             sub new {
26 0     0 1 0 state $rule = Data::Validator->new(
27             client_id => { isa => 'Str' },
28             client_secret => { isa => 'Str' },
29             refresh_token => { isa => 'Str' },
30              
31             spreadsheet_id => { isa => 'Str' },
32             timeout => { isa => 'Int', default => 120 },
33             )->with('Method','AllowExtra');
34 0         0 my($class, $args) = $rule->validate(@_);
35              
36             my $self = bless {
37             %$args,
38             ua => undef,
39             csv => Text::CSV->new({ binary => 1}),
40             endpoint => 'https://sheets.googleapis.com/v4/spreadsheets/'.$args->{spreadsheet_id},
41 0         0 }, $class;
42              
43 0         0 $self->_initialize;
44              
45 0         0 return $self;
46             }
47              
48             sub _initialize {
49 0     0   0 my($self) = @_;
50              
51 0         0 my $account = {
52             auth_provider_x509_cert_url => 'https://www.googleapis.com/oauth2/v1/certs',
53             auth_uri => 'https://accounts.google.com/o/oauth2/auth',
54             redirect_uris => [
55             'urn:ietf:wg:oauth:2.0:oob',
56             'http://localhost'
57             ],
58             token_uri => 'https://accounts.google.com/o/oauth2/token',
59             };
60              
61 0         0 for my $f (qw(client_id client_secret refresh_token)) {
62 0         0 $account->{$f} = $self->{$f};
63             }
64              
65             my $oauth2 = Net::Google::DataAPI::Auth::OAuth2->new(
66             client_id => $account->{client_id},
67             client_secret => $account->{client_secret},
68 0         0 scope => [qw(
69             https://www.googleapis.com/auth/drive
70             https://www.googleapis.com/auth/drive.readonly
71             https://www.googleapis.com/auth/spreadsheets
72             https://www.googleapis.com/auth/spreadsheets.readonly
73             )],
74             );
75              
76 0         0 my $ow = $oauth2->oauth2_webserver;
77             my $token = Net::OAuth2::AccessToken->new(
78             profile => $ow,
79             auto_refresh => 1,
80             refresh_token => $account->{refresh_token},
81 0         0 );
82 0         0 $ow->update_access_token($token);
83 0         0 $token->refresh;
84 0         0 $oauth2->access_token($token);
85              
86             $self->{ua} = Furl->new(
87             headers => [ 'Authorization' => sprintf('Bearer %s', $token->access_token) ],
88             timeout => $self->{timeout},
89 0         0 );
90             }
91              
92             sub request {
93 0     0 0 0 my($self, $method, $url, $content, $opt) = @_;
94              
95             $opt = {
96             retry_times => 3,
97             retry_interval => 1.0,
98 0   0     0 %{ $opt // {} },
  0         0  
99             };
100              
101 0         0 $url = $self->endpoint . $url;
102              
103 0   0     0 debugf("request: %s => %s %s %s", $method, $url, ddf($content//'{no content}'), ddf($opt//'no opt'));
      0        
104              
105 0         0 my $headers = [];
106 0 0       0 if ($content) {
107 0         0 push @$headers, 'Content-Type' => 'application/json';
108             }
109 0 0       0 if ($opt->{headers}) {
110 0         0 push @$headers, @{ $opt->{headers} };
  0         0  
111             }
112              
113             my $res = retry $opt->{retry_times}, $opt->{retry_interval}, sub {
114 0 0   0   0 $self->ua->request(
115             method => $method,
116             url => $url,
117             headers => $headers,
118             $content ? (content => encode_json($content)) : (),
119             );
120             }, sub {
121 0     0   0 my $res = shift;
122 0 0 0     0 if (!$res) {
    0          
123 0         0 warnf "not HTTP::Response: $@";
124 0         0 return 1;
125             } elsif ($res->status_line =~ /^500\s+Internal Response/
126             or $res->code =~ /^50[234]$/
127             ) {
128 0         0 warnf 'retrying: %s', $res->status_line;
129 0         0 return 1; # do retry
130             } else {
131 0         0 return;
132             }
133 0         0 };
134              
135 0 0       0 if (!$res) {
136 0   0     0 critf 'failure %s %s %s', $method, $url, ddf($content//'{no content}');
137 0         0 return;
138             } else {
139 0 0       0 if ($res->is_success) {
140 0 0       0 my $res_content = $res->decoded_content ? decode_json($res->decoded_content) : 1;
141 0 0       0 return wantarray ? ($res_content, $res) : $res_content;
142             } else {
143 0   0     0 critf 'failure %s %s %s: %s', $method, $url, ddf($content//'{no content}'), $res->status_line;
144 0 0       0 return wantarray ? ('', $res) : '';
145             }
146             }
147             }
148              
149             sub get_sheet {
150 0     0 1 0 state $rule = Data::Validator->new(
151             title => { isa => 'Str', xor => [qw(index sheet_id)] },
152             index => { isa => 'Str', xor => [qw(title sheet_id)] },
153             sheet_id => { isa => 'Str', xor => [qw(title index )] },
154             )->with('Method');
155 0         0 my($self, $args) = $rule->validate(@_);
156              
157 0         0 my($pkey, $akey);
158 0         0 for my $key (qw(title index sheet_id)) {
159 0 0       0 next unless exists $args->{$key};
160 0         0 $akey = $key;
161             $pkey = {
162             sheet_id => 'sheetId',
163 0   0     0 }->{$key} // $key;
164             }
165              
166 0         0 my($content) = $self->request(GET => '');
167 0         0 for my $sheet (@{ $content->{sheets} }) {
  0         0  
168 0 0       0 if ($sheet->{properties}{$pkey} eq $args->{$akey}) {
169 0         0 return $sheet;
170             }
171             }
172              
173 0         0 return;
174             }
175              
176             sub clear_sheet {
177 0     0 1 0 state $rule = Data::Validator->new(
178             sheet_id => { isa => 'Str' },
179             )->with('Method');
180 0         0 my($self, $args) = $rule->validate(@_);
181              
182             return $self->request(
183             POST => ':batchUpdate',
184             {
185             requests => [
186             {
187             repeatCell => {
188             range => {
189             sheetId => $args->{sheet_id},
190             },
191 0         0 cell => {
192             },
193             fields => '*',
194             },
195             },
196             ],
197             },
198             );
199             }
200              
201             # see:
202             # https://developers.google.com/sheets/guides/concepts#a1_notation
203             # t/02_a1_notation.t
204             sub a1_notation {
205 6     6 0 3528 state $rule = Data::Validator->new(
206             sheet_title => { isa => 'Str', optional => 1 },
207             start_column => { isa => 'Int', optional => 1 },
208             end_column => { isa => 'Int', optional => 1 },
209             start_row => { isa => 'Int', optional => 1 },
210             end_row => { isa => 'Int', optional => 1 },
211             )->with('Method');
212 6         2996 my($self, $args) = $rule->validate(@_);
213              
214 6         611 my($sheet_title, $start, $end) = ('', '', '');
215 6 100       19 if (exists $args->{sheet_title}) {
216 5         10 $sheet_title = $args->{sheet_title};
217 5         24 $sheet_title =~ s/'/''/g;
218 5         21 $sheet_title = sprintf(q{'%s'}, $sheet_title);
219             }
220              
221 6 100       16 if (exists $args->{start_column}) {
222 4         14 $start .= $self->column_notation($args->{start_column});
223             }
224 6 100       18 if (exists $args->{start_row}) {
225 4         9 $start .= $args->{start_row};
226             }
227              
228 6 100       14 if (exists $args->{end_column}) {
229 4         9 $end .= $self->column_notation($args->{end_column});
230             }
231 6 100       13 if (exists $args->{end_row}) {
232 3         6 $end .= $args->{end_row};
233             }
234              
235 6 100 66     24 if (not $sheet_title) {
    100          
236 1         5 return join(':', $start, $end);
237             } elsif (not $start and not $end) {
238 1         5 return $sheet_title;
239             } else {
240 4         25 return sprintf('%s!%s', $sheet_title, join(':', $start, $end));
241             }
242             }
243              
244             sub column_notation {
245 8     8 0 15 my($self, $n) = @_;
246              
247 8         22 my $l = int($n / 27);
248 8         14 my $r = $n - $l * 26;
249              
250 8 50       17 if ($l > 0) {
251 0         0 return pack 'CC', $l+64, $r+64;
252             } else {
253 8         29 return pack 'C', $r+64;
254             }
255             }
256              
257             sub to_csv {
258 0     0 1   my $self = shift;
259              
260 0           my $status = $self->csv->combine(@_);
261 0 0         return $status ? $self->csv->string() : ();
262             }
263              
264             1;
265              
266             __END__
267              
268             =encoding utf8
269              
270             =begin html
271              
272             <a href="https://travis-ci.org/hirose31/Net-Google-Spreadsheets-V4"><img src="https://travis-ci.org/hirose31/Net-Google-Spreadsheets-V4.png?branch=master" alt="Build Status" /></a>
273             <a href="https://coveralls.io/r/hirose31/Net-Google-Spreadsheets-V4?branch=master"><img src="https://coveralls.io/repos/hirose31/Net-Google-Spreadsheets-V4/badge.png?branch=master" alt="Coverage Status" /></a>
274              
275             =end html
276              
277             =head1 NAME
278              
279             Net::Google::Spreadsheets::V4 - Google Sheets API v4 client
280              
281             =begin readme
282              
283             =head1 INSTALLATION
284              
285             To install this module, run the following commands:
286              
287             perl Build.PL
288             ./Build
289             ./Build test
290             ./Build install
291              
292             =end readme
293              
294             =head1 SYNOPSIS
295              
296             use Net::Google::Spreadsheets::V4;
297            
298             my $gs = Net::Google::Spreadsheets::V4->new(
299             client_id => "YOUR_CLIENT_ID",
300             client_secret => "YOUR_CLIENT_SECRET",
301             refresh_token => "YOUR_REFRESH_TOKEN",
302            
303             spreadsheet_id => "YOUR_SPREADSHEET_ID",
304             );
305            
306             my ($content, $res) = $gs->request(
307             POST => ':batchUpdate',
308             {
309             requests => [ ... ],
310             },
311             );
312              
313             See also examples/import.pl for more complex code.
314              
315             =head1 DESCRIPTION
316              
317             Net::Google::Spreadsheets::V4 is Google Sheets API v4 client
318              
319             =head1 METHODS
320              
321             =head2 Class Methods
322              
323             =head3 B<new>(%args:Hash) :Net::Google::Spreadsheets::V4
324              
325             Creates and returns a new Net::Google::Spreadsheets::V4 client instance. Dies on errors.
326              
327             %args is following:
328              
329             =over 4
330              
331             =item client_id => Str
332              
333             =item client_secret => Str
334              
335             =item refresh_token => Str
336              
337             =item spreadsheet_id => Str
338              
339             =back
340              
341             =head2 Instance Methods
342              
343             =head3 B<get_sheet>(title|index|sheet_id => Str) :HashRef
344              
345             Get C<Sheet> object by title or index or sheet_id.
346              
347             =head3 B<clear_sheet>(sheet_id => Str)
348              
349             Delete all data.
350              
351             =head3 B<to_csv>(Array)
352              
353             Convert Array to CSV Str.
354              
355             =head1 AUTHOR
356              
357             HIROSE Masaaki E<lt>hirose31@gmail.comE<gt>
358              
359             =head1 REPOSITORY
360              
361             L<https://github.com/hirose31/Net-Google-Spreadsheets-V4>
362              
363             git clone https://github.com/hirose31/Net-Google-Spreadsheets-V4.git
364              
365             patches and collaborators are welcome.
366              
367             =head1 SEE ALSO
368              
369             L<https://developers.google.com/sheets/>
370              
371             =head1 COPYRIGHT
372              
373             Copyright HIROSE Masaaki
374              
375             =head1 LICENSE
376              
377             This library is free software; you can redistribute it and/or modify
378             it under the same terms as Perl itself.
379              
380             =cut
381              
382             # for Emacsen
383             # Local Variables:
384             # mode: cperl
385             # cperl-indent-level: 4
386             # cperl-close-paren-offset: -4
387             # cperl-indent-parens-as-block: t
388             # indent-tabs-mode: nil
389             # coding: utf-8
390             # End:
391              
392             # vi: set ts=4 sw=4 sts=0 et ft=perl fenc=utf-8 ff=unix :