File Coverage

blib/lib/Google/Fusion.pm
Criterion Covered Total %
statement 51 122 41.8
branch 0 28 0.0
condition 0 6 0.0
subroutine 17 23 73.9
pod 4 4 100.0
total 72 183 39.3


line stmt bran cond sub pod time code
1             package Google::Fusion;
2 1     1   21982 use 5.006;
  1         5  
  1         43  
3              
4 1     1   2315 use Moose;
  1         570267  
  1         9  
5 1     1   7378 use Moose::Util::TypeConstraints;
  1         7  
  1         12  
6 1     1   3925 use MooseX::Params::Validate;
  1         18646  
  1         8  
7 1     1   3790 use LWP::UserAgent;
  1         92246  
  1         56  
8 1     1   15 use HTTP::Request;
  1         3  
  1         38  
9 1     1   1995 use URL::Encode qw/url_encode/;
  1         22135  
  1         108  
10 1     1   9754 use YAML qw/LoadFile DumpFile Dump/;
  1         21699  
  1         113  
11 1     1   15 use Carp;
  1         3  
  1         71  
12 1     1   1307 use Net::OAuth2::Moosey::Client;
  1         929588  
  1         58  
13 1     1   873 use Google::Fusion::Result;
  1         5  
  1         49  
14 1     1   2790 use Text::CSV;
  1         14364  
  1         9  
15 1     1   52 use Time::HiRes qw/time/;
  1         2  
  1         17  
16 1     1   246 use Try::Tiny;
  1         4  
  1         88  
17 1     1   1672 use Digest::SHA qw/sha256_hex/;
  1         5087  
  1         134  
18 1     1   1112 use File::Spec::Functions;
  1         1028  
  1         106  
19 1     1   1682 use IO::String;
  1         2969  
  1         1598  
20              
21             =head1 NAME
22              
23             Google::Fusion - Interface to the Google Fusion Tables API
24              
25             =head1 VERSION
26              
27             Version 0.10
28              
29             =cut
30              
31             our $VERSION = '0.10';
32              
33              
34             =head1 SYNOPSIS
35              
36             my $fusion = Google::Fusion->new(
37             client_id => $client_id,
38             client_secret => $client_secret,
39             token_store => $token_store,
40             );
41              
42             # Get the result for a query
43             my $result = $fusion->query( $sql );
44            
45             # Print out the rows returned
46             foreach( @{ $result->rows } ){
47             print join( ',', @{ $_ } ) . "\n";
48             }
49              
50             =head1 PARAMS/ACCESSORS
51              
52             One of the following combination of parameters is required:
53              
54             client_id and client_secret
55              
56             You will be prompted with a URL, with which you will atain an access_code.
57              
58             client_id, client_secret, access_code
59              
60             The OAuth2 client will complete the authorization process for you and get the refresh_token and access_token for you
61              
62             refresh_token and optionally access_token
63              
64             The OAuth2 client will get a valid access_token for you if necessary, and refresh it when necessary.
65              
66             access_token
67              
68             You will be able to make requests as long as the access_token is valid.
69              
70             =over 2
71              
72             =item * client_id <Str>
73              
74             The client id of your application.
75              
76             =item * client_secret <Str>
77              
78             The secret for your application
79              
80             =item * refresh_token <Str>
81              
82             Refresh token.
83             Can be defined here, otherwise it will be aquired during the authorization process
84              
85             =item * access_token <Str>
86              
87             A temporary access token aquired during the authorization process
88             Can be defined here, otherwise it will be aquired during the authorization process
89              
90             =item * keep_alive <Int>
91              
92             Use keep_alive for connections - this will make the application /much/ more responsive.
93              
94             Default: 1
95              
96             =item * headers <Bool>
97              
98             Responses passed with headers.
99              
100             Default: 1
101              
102             =item * access_code <Str>
103              
104             The code returned during the OAuth2 authorization process with which access_token and refresh_token are aquired.
105              
106             =item * auth_client
107              
108             A Net::OAuth2::Moosey::Client object with which authenticated requests are made. If you are running
109             in application mode (interactive), then you can accept the default.
110             If you already have an authenticated client, then initialise with it.
111             If you have some required parameters (access_token, refresh_token or access_code), but no client
112             object yet, then just define these parameters, and allow the client to be created for you.
113              
114             =item * query_cache <Str>
115              
116             Path to a directory to use as a query cache. This can be used to cache your results for blazing
117             fast performance, and not actually hitting google for every query when testing, but should not
118             be enabled in a productive environment otherwise you will have stale content.
119              
120             =item * token_store <Str>
121              
122             Path to the token store file to store access/refresh tokens
123              
124             =back
125              
126             =cut
127             subtype 'InsertStatement',
128             as 'Str',
129             where { $_ =~ m/^INSERT/ };
130              
131             has 'client_id' => ( is => 'ro', isa => 'Str', );
132             has 'client_secret' => ( is => 'ro', isa => 'Str', );
133             has 'refresh_token' => ( is => 'ro', isa => 'Str', );
134             has 'access_token' => ( is => 'ro', isa => 'Str', );
135             has 'access_code' => ( is => 'ro', isa => 'Str', );
136             has 'query_cache' => ( is => 'ro', isa => 'Str', );
137             has 'token_store' => ( is => 'ro', isa => 'Str', );
138             has 'headers' => ( is => 'ro', isa => 'Bool', required => 1, default => 1, );
139             has 'keep_alive' => ( is => 'ro', isa => 'Bool', required => 1, default => 1, );
140             has 'auth_client' => ( is => 'ro', required => 1, lazy => 1,
141             isa => 'Net::OAuth2::Moosey::Client',
142             builder => '_build_auth_client',
143             );
144             has 'insert_buffer' => ( is => 'rw', isa => 'Str', clearer => 'clear_insert_buffer', default => sub{ '' } );
145             has 'insert_buffer_limit' => ( is => 'ro', isa => 'Int', default => 20_000 );
146              
147             # Local method to build the auth_client if it wasn't passed
148             sub _build_auth_client {
149 0     0     my $self = shift;
150              
151 0           my %client_params = (
152             site_url_base => 'https://accounts.google.com/o/oauth2/auth',
153             access_token_url_base => 'https://accounts.google.com/o/oauth2/token',
154             authorize_url_base => 'https://accounts.google.com/o/oauth2/auth',
155             scope => 'https://www.google.com/fusiontables/api/query',
156             );
157 0           foreach( qw/client_id client_secret refresh_token access_code access_token keep_alive token_store/ ){
158 0 0         $client_params{$_} = $self->$_ if defined $self->$_;
159             }
160            
161             # $self->logger->debug( "Initialising Client with:\n". Dump( \%client_params ) );
162 0           my $client = Net::OAuth2::Moosey::Client->new( %client_params );
163 0           return $client;
164             }
165              
166             =head1 METHODS
167              
168             =head2 query
169              
170             Submit a (Googley) SQL query. Single argument is the SQL.
171             Return is a L<Google::Fusion::Result> object
172              
173             Example:
174              
175             my $result = $fusion->query( 'SELECT * FROM 123456' );
176              
177             =cut
178             sub query {
179 0     0 1   my $self = shift;
180 0           my $sql = shift;
181              
182             # Get a valid access_token before timing the query time
183 0           my $auth_time_start = time();
184 0           $self->auth_client->access_token_object->valid_access_token();
185 0           my $auth_time = time() - $auth_time_start;
186              
187 0           my $query_start = time();
188            
189 0           my $response = $self->_query_or_cache( $sql );
190              
191 0           my $query_time = time() - $query_start;
192 0           my $result = Google::Fusion::Result->new(
193             query => $sql,
194             response => $response,
195             query_time => $query_time,
196             auth_time => $auth_time,
197             total_time => $query_time + $auth_time,
198             );
199              
200 0 0         if( not $response->is_success ){
201 0           $result->error( sprintf "%s (%u)", $response->message, $response->code );
202             }else{
203             # Response was a success
204             # TODO: RCL 2011-09-08 Parse the actual error message from the response
205             # TODO: RCL 2011-09-08 Refresh access_key if it was invalid, or move that
206             # action to the Client?
207              
208 0           my $data = $response->decoded_content();
209             # print $data;
210 0 0         my $csv = Text::CSV->new ( {
211             binary => 1, # Reliable handling of UTF8 characters
212             escape_char => '"',
213             quote_char => '"',
214             } ) or croak( "Cannot use CSV: ".Text::CSV->error_diag () );
215 0           my $io = IO::String->new( $data );
216 0           my $parsed_data = $csv->getline_all( $io );
217 0 0         $csv->eof or $csv->error_diag();
218              
219              
220             # Find the max length of each column
221             # TODO: RCL 2011-09-09 This won't handle elements with newlines gracefully...
222 0           my @max;
223 0           foreach my $row_idx( 0 .. scalar( @{ $parsed_data } ) - 1 ){
  0            
224 0           foreach my $col_idx ( 0 .. scalar( @{ $parsed_data->[0] } ) - 1 ){
  0            
225 0 0 0       if( ( not $max[$col_idx] ) or ( length( $parsed_data->[$row_idx][$col_idx] ) > $max[$col_idx] ) ){
226 0           $max[$col_idx] = length( $parsed_data->[$row_idx][$col_idx] );
227             }
228             }
229             }
230              
231              
232 0 0         if( $self->headers ){
233 0           $result->columns( shift( @{ $parsed_data } ) );
  0            
234             }
235 0           $result->rows( $parsed_data );
236 0           $result->has_headers( $self->headers );
237 0 0         if( not $result->num_columns ){
238 0           $result->num_columns( scalar( @{ $parsed_data->[0] } ) );
  0            
239             }
240 0           $result->max_lengths( \@max );
241 0           $result->has_headers( $self->headers );
242             }
243 0           return $result;
244             }
245              
246             =head2 get_fresh_access_token
247              
248             Force the OAuth access token to be refreshed
249              
250             Example:
251              
252             $fusion->get_fresh_access_token();
253              
254             =cut
255             sub get_fresh_access_token {
256 0     0 1   my $self = shift;
257 0           $self->auth_client->get_fresh_access_token();
258             }
259              
260              
261             =head2 add_to_insert_buffer
262              
263             The Fusion Table intefrace allows multiple INSERT commands to be sent in one query.
264             This can be used similarly to the query method, but and will reduce the number of queries
265             you use.
266              
267            
268             # Get data from your local database
269             $sth->execute();
270             while( my $row = $sth->fetchrow_hashref ){
271             $fusion->add_to_insert_buffer( sprintf( "INSERT INTO INTO 12345 ( Id, Text ) VALUES( '%s', '%s' )", $row->{Id}, $row->{Text} ) );
272             }
273             $fusion->send_insert_buffer;
274              
275             Obviously this can be further optimised by having many VALUES per INSERT.
276              
277             If a send_insert_buffer was triggered during the add, this is returned, otherwise undef is returned
278              
279             =cut
280             sub add_to_insert_buffer {
281 0     0 1   my $self = shift;
282 0           my ( $sql ) = pos_validated_list(
283             \@_,
284             { isa => 'InsertStatement' },
285             );
286            
287             # Make sure there is a newline after the new SQL
288 0           $sql =~ s/^(.*);?\s*/$1;\n/s;
289              
290 0           my $rtn = undef;
291             # Send the buffer if it is already full
292 0 0         if( ( length( $sql ) + length( $self->insert_buffer ) ) > $self->insert_buffer_limit ){
293 0           $rtn = $self->send_insert_buffer;
294             }
295 0           $self->insert_buffer( $self->insert_buffer . $sql );
296 0           return $rtn;
297             }
298              
299             =head2 send_insert_buffer
300              
301             Flush the rest of the insert buffer
302              
303             =cut
304             sub send_insert_buffer {
305 0     0 1   my $self = shift;
306 0           my $sql = $self->insert_buffer;
307 0           $self->clear_insert_buffer;
308 0           return $self->query( $sql );
309             }
310              
311             # Private method to use cached queries if possible (and desired - the query_cache is defined)
312             sub _query_or_cache {
313 0     0     my $self = shift;
314 0           my $sql = shift;
315 0           my $digest = sha256_hex( $sql );
316             # printf "Digest: %s\n", $digest;
317 0           my $cache_file = catfile( $self->query_cache, $digest );
318            
319 0           my $response = undef;
320 0 0         if( $self->query_cache ){
321 0 0         if( -f $cache_file ){
322 0           $response = LoadFile( $cache_file );
323             }
324             }
325 0 0         if( not $response ){
326 0 0         my @post_args = ( 'https://www.google.com/fusiontables/api/query',
327             HTTP::Headers->new( Content_Type => 'application/x-www-form-urlencoded' ),
328             sprintf( 'sql=%s&hdrs=%s',
329             url_encode( $sql ),
330             ( $self->headers ? 'true' : 'false' ),
331             ),
332             );
333              
334              
335 0           $response = $self->auth_client->post( @post_args );
336             # If the response was not Unauthorized, most likely is that the token is invalid
337             # Invalidate the current token, and try again
338 0 0 0       if( $response->code == 401 and $response->message eq 'Unauthorized' ){
339             # Make the token expire, so a new one is requested
340 0           $self->auth_client->get_fresh_access_token();
341 0           $response = $self->auth_client->post( @post_args );
342             }
343              
344 0 0         if( $self->query_cache ){
345 0           DumpFile( $cache_file, $response );
346             }
347             }
348 0           return $response;
349             }
350              
351              
352             =head1 AUTHOR
353              
354             Robin Clarke, C<< <perl at robinclarke.net> >>
355              
356             =head1 BUGS
357              
358             Please report any bugs or feature requests to C<bug-google-fusion at rt.cpan.org>, or through
359             the web interface at L<http://rt.cpan.org/NoAuth/ReportBug.html?Queue=Google-Fusion>. I will be notified, and then you'll
360             automatically be notified of progress on your bug as I make changes.
361              
362             =head1 SUPPORT
363              
364             You can find documentation for this module with the perldoc command.
365              
366             perldoc Google::Fusion
367              
368              
369             You can also look for information at:
370              
371             =over 4
372              
373             =item * Repository on Github
374              
375             L<https://github.com/robin13/Google-Fusion>
376              
377             =item * RT: CPAN's request tracker (report bugs here)
378              
379             L<http://rt.cpan.org/NoAuth/Bugs.html?Dist=Google-Fusion>
380              
381             =item * AnnoCPAN: Annotated CPAN documentation
382              
383             L<http://annocpan.org/dist/Google-Fusion>
384              
385             =item * CPAN Ratings
386              
387             L<http://cpanratings.perl.org/d/Google-Fusion>
388              
389             =item * Search CPAN
390              
391             L<http://search.cpan.org/dist/Google-Fusion/>
392              
393             =back
394              
395              
396             =head1 ACKNOWLEDGEMENTS
397              
398              
399             =head1 LICENSE AND COPYRIGHT
400              
401             Copyright 2011 Robin Clarke.
402              
403             This program is free software; you can redistribute it and/or modify it
404             under the terms of either: the GNU General Public License as published
405             by the Free Software Foundation; or the Artistic License.
406              
407             See http://dev.perl.org/licenses/ for more information.
408              
409              
410             =cut
411              
412             1; # End of Google::Fusion