File Coverage

blib/lib/WWW/KeenIO.pm
Criterion Covered Total %
statement 35 98 35.7
branch 0 30 0.0
condition 0 23 0.0
subroutine 12 19 63.1
pod 3 4 75.0
total 50 174 28.7


line stmt bran cond sub pod time code
1             package WWW::KeenIO;
2              
3 5     5   140861 use 5.006;
  5         22  
4 5     5   35 use strict;
  5         13  
  5         127  
5 5     5   30 use warnings;
  5         51  
  5         335  
6              
7             =head1 NAME
8              
9             WWW::KeenIO - Perl API for Keen.IO L<< http://keen.io >> event storage and analytics
10              
11             =head1 VERSION
12              
13             Version 0.01
14              
15             =cut
16              
17             our $VERSION = '0.01';
18              
19 5     5   28 use Carp qw(cluck);
  5         10  
  5         374  
20 5     5   5621 use Data::Dumper;
  5         51996  
  5         342  
21 5     5   4647 use REST::Client;
  5         308878  
  5         201  
22 5     5   5516 use JSON::XS;
  5         54227  
  5         425  
23 5     5   47 use URI;
  5         12  
  5         170  
24 5     5   29 use Scalar::Util qw(blessed reftype);
  5         11  
  5         500  
25 5     5   4714 use Readonly;
  5         17870  
  5         287  
26 5     5   31 use Exporter 'import';
  5         10  
  5         150  
27              
28 5     5   3724 use Mouse;
  5         186596  
  5         29  
29              
30             #** @attr public api_key $api_key API access key
31             #*
32             has api_key => ( isa => 'Str', is => 'rw', required => 1 );
33              
34             #** @attr public CodeRef $read_key API key for writing data (if different from api_key
35             #*
36             has write_key => ( isa => 'Maybe[Str]', is => 'rw' );
37              
38             #** @attr public Int $project_id ID of the project
39             #*
40             has project => ( isa => 'Str', is => 'rw', required => 1 );
41              
42             #** @attr protected String $base_url Base REST URL
43             #*
44             has base_url => (
45             isa => 'Str',
46             is => 'rw',
47             default => 'https://api.keen.io/3.0/'
48             );
49              
50             Readonly my $REST_data => {
51             put => {
52             path => 'projects/$project/events/$collection',
53             write => 1
54             },
55             batch_put => {
56             path => 'projects/$project/events',
57             write => 1
58             },
59             select => {
60             path => 'projects/$project/queries/extraction'
61             }
62             };
63              
64             Readonly our $KEEN_OP_EQ => 'eq';
65             Readonly our $KEEN_OP_NE => 'ne';
66             Readonly our $KEEN_OP_EXISTS => 'exists';
67             Readonly our $KEEN_OP_IN => 'exists';
68             Readonly our $KEEN_OP_CONTAINS => 'contains';
69              
70             my @operators = qw($KEEN_OP_EQ $KEEN_OP_NE $KEEN_OP_EXISTS $KEEN_OP_IN
71             $KEEN_OP_CONTAINS);
72             our @EXPORT_OK = (@operators);
73             our %EXPORT_TAGS = ( 'operators' => [@operators] );
74              
75             #** @attr protected CodeRef $ua Reference to the REST UA
76             #*
77             has ua => (
78             isa => 'Object',
79             is => 'rw',
80             lazy => 1,
81             init_arg => undef,
82             default => sub {
83             return REST::Client->new();
84             }
85             );
86              
87             #** @attr public String $error_message Error message regarding the last failed operation
88             #*
89             has error_message =>
90             ( isa => 'Str', is => 'rw', init_arg => undef, default => '' );
91              
92             sub _url {
93 0     0     my ( $self, $path, $url_params, $query_params ) = @_;
94              
95 0   0       $url_params //= {};
96             $url_params->{project} = $self->project
97 0 0         unless defined( $url_params->{project} );
98 0   0       $query_params //= {};
99 0           my $url = $self->base_url . $path;
100 0   0       $url =~ s^\$([\w\d\_]+)^$url_params->{$1} // ''^eg;
  0            
101 0           my $uri = URI->new( $url, 'http' );
102 0           $uri->query_form($query_params);
103             #print "URL=".$uri->as_string;
104 0           return $uri->as_string;
105             }
106              
107             sub _process_response {
108 0     0     my ( $self, $response ) = @_;
109              
110 0 0         if ($@) {
    0          
111 0           $self->error_message("Error $@");
112 0           return undef;
113             } elsif ( !blessed($response) ) {
114 0           $self->error_message(
115             "Unknown response $response from the REST client instead of object"
116             );
117 0           return undef;
118             }
119             print "Got response:"
120             . Dumper( $response->responseCode() ) . "/"
121 0 0         . Dumper( $response->responseContent() ) . "\n" if $ENV{DEBUG};
122 0           my $code = $response->responseCode();
123 0           my $parsed_content = eval { decode_json( $response->responseContent() ) };
  0            
124 0 0         if ($@) {
125 0           cluck( "Cannot parse response content "
126             . $response->responseContent()
127             . ", error msg: $@. Is this JSON?" );
128 0           $parsed_content = {};
129             }
130 0 0         print "parsed ".Dumper($parsed_content) if $ENV{DEBUG};
131 0 0 0       if ( $code ne '200' && $code ne '201' ) {
132 0           my $err = "Received error code $code from the server instead of "
133             . 'expected 200/201';
134 0 0 0       if ( reftype($parsed_content) eq 'HASH'
135             && $parsed_content->{message} )
136             {
137             $err .=
138             "\nError message from KeenIO: "
139             . $parsed_content->{message}
140             . ( $parsed_content->{error_code}
141 0 0         ? ' (' . $parsed_content->{error_code} . ')'
142             : q{} );
143              
144 0           $self->error_message($err);
145             }
146 0           return undef;
147             }
148              
149 0           $self->error_message(q{});
150 0           return $parsed_content;
151             }
152              
153             sub _transaction {
154 0     0     my ( $self, $query_params, $data ) = @_;
155              
156 0           my $caller_sub = ( split( '::', ( caller(1) )[3] ) )[-1];
157 0           my $rest_data = $REST_data->{$caller_sub};
158              
159 0   0       $data //= {};
160             my $key =
161             $rest_data->{write}
162 0 0 0       ? ( $self->write_key // $self->api_key )
163             : $self->api_key;
164 0           my $method_path = $rest_data->{path};
165 0 0         confess("No URL path defined for method $caller_sub") unless $method_path;
166              
167 0           my $url = $self->_url( $method_path, $query_params );
168 0           my $headers = {
169             'Content-Type' => 'application/json',
170             Authorization => $key
171             };
172             my $response =
173 0           eval { $self->ua->POST( $url, encode_json($data), $headers ); };
  0            
174 0 0         cluck($@) if $@;
175 0           return $self->_process_response($response);
176             }
177              
178             =head1 SYNOPSIS
179              
180             use WWW::KeenIO qw(:operators);
181             use Text::CSV_XS;
182             use Data::Dumper;
183              
184             my $csv = Text::CSV_XS->new;
185             my $keen = WWW::KeenIO->new( {
186             project => '123',
187             api_key => '456',
188             write_key => '789'
189             }) or die 'Cannot create KeenIO object';
190              
191             # process a CSV file with 3 columns: name, in|out, date-time
192             # import them as keenIO events
193             while(<>) {
194             chomp;
195             my $status = $csv->parse($_);
196             unless ($status) {
197             warn qq{Cannot parse '$_':}.$csv->error_diag();
198             next;
199             }
200             my @fields = $csv->fields();
201             my $data = {
202             keen => {
203             timestamp => $fields[2]
204             },
205             name => $fields[0],
206             type => $fields[1]
207             };
208             my $res = $keen->put('in_out_log', $data);
209             unless ($res) {
210             warn "Unable to store the data in keenIO";
211             }
212             }
213            
214             # now read the data
215             my $data = $keen->select('in_out_log', 'this_7_days',
216             [ $keen->filter('name', $KEEN_OP_EQ, 'John Doe') ] );
217             print Dumper($data);
218              
219             =head1 CONSTRUCTOR
220              
221             =head2 new( hashref )
222              
223             Creates a new object, acceptable parameters are:
224              
225             =over 16
226              
227             =item C - (required) the key to be used for read operations
228              
229             =item C - (required) the ID of KeenIO project
230              
231             =item C - the key to be used for write operations (if different from api_key)
232              
233             =item C - L<< https://api.keen.io/3.0/ >> by default; in case if you are using KeenIO-compatible API on some other server you can specify your own URL here
234              
235             =back
236              
237             =head1 METHODS
238              
239             =head2 put( $collection_name, $data )
240              
241             Inserts an event (C<$data> is a hashref) into the collection. Returns a
242             reference to a hash, which contains the response
243             received from the server (typically there is a key C with
244             C value). Returns C on failure, application then may call
245             C method to get the detailed info about the error.
246              
247             my $res = $keen->put('in_out_log', $data);
248             unless ($res) {
249             warn 'Something went wrong '.$keen->error_message();
250             }
251              
252             =cut
253              
254             sub put {
255 0     0 1   my ( $self, $collection, $record ) = @_;
256 0           return $self->_transaction(
257             {
258             collection => $collection
259             },
260             $record
261             );
262             }
263              
264             =head2 batch_put( $data )
265              
266             Inserts multiple events into Keen. C<$data> is a hashref, where every key
267             represents a collection name, where data should be inserted. Value of
268             the key is a reference to an array, which contains references to
269             individual event data (hashes).
270              
271             Returns C on total failure (e.g. unable to access the servers). Otherwise
272             returns a reference to a hash; each key represents a collection name and the
273             value is a reference to an array of statuses for individual events.
274              
275             my $res = $keen->batch_put( {
276             payments => [
277             {
278             name => 'John Doe',
279             customer_id => 123,
280             amount => 35.00
281             },
282             {
283             name => 'Peter Smith',
284             customer_id => 125,
285             amount => '10.00'
286             }
287             ],
288             purchases => [
289             {
290             name => 'John Doe',
291             customer_id => 123,
292             product_id => 567,
293             quantity => 1,
294             date => '2015-11-01 15:06:34'
295             }
296             ]
297             });
298             unless ($res) {
299             warn 'Something went wrong '.$keen->error_message();
300             }
301              
302             =cut
303              
304             sub batch_put {
305 0     0 1   my ( $self, $data ) = @_;
306 0           return $self->_transaction( { }, $data );
307             }
308              
309             =head2 get($collection_name, $interval [, $filters ] )
310              
311             Retrieves a list of events from the collection. C<$collection_name> is
312             self-explanatory. C<$interval> is a string, which describes the time period
313             we are interested in (see L<< https://keen.io/docs/api/#timeframe) >>).
314             C<$filters> is optional. If provided - should be an arrayref, each element
315             is an additional condition according to L<< https://keen.io/docs/api/#query-parameters >>.
316              
317             Returns a reference to an array on hashrefs; each element is a reference
318             to an actual events. Upon failure returns C.
319              
320             my $data = $keen->select('in_out_log', 'this_7_days',
321             [ $keen->filter('name', $KEEN_OP_EQ, 'John Doe') ]);
322             print Dumper($data);
323              
324             =cut
325              
326             sub select {
327 0     0 0   my ( $self, $collection, $timeframe, $filters ) = @_;
328              
329 0 0 0       unless ( defined($collection) && defined($timeframe) ) {
330 0           $self->error_message("Must provide collection name and timeframe");
331 0           return undef;
332             }
333              
334 0           my $params = {};
335 0 0         $params->{filters} = $filters if $filters;
336 0           $params->{event_collection} = $collection;
337 0           $params->{timeframe} = $timeframe;
338 0           my $x = $self->_transaction( {}, $params );
339 0 0 0       unless ( reftype($x) eq 'HASH' && reftype( $x->{result} ) eq 'ARRAY' ) {
340 0           return undef;
341             }
342 0           return $x->{result};
343             }
344              
345             =head2 filter($field, $operator, $value)
346              
347             Creates a filter for retrieving events via select() method.
348              
349             use WWW::KeenIO qw(:operators);
350             my $res = $keen->select('tests', 'this_10_years', [
351             $keen->filter('Author', $KEEN_OP_CONTAINS, 'Andrew'),
352             $keen->filter('Status', $KEEN_OP_EQ, 'resolved')
353             ] );
354              
355             Please refer to Keen API documentation regarding all available operators and
356             their usage. For convenience constants for most frequently used operators are exported via :operators tag:
357             $KEEN_OP_EQ, $KEEN_OP_NE, $KEEN_OP_EXISTS, $KEEN_OP_IN, $KEEN_OP_CONTAINS
358              
359             =cut
360              
361             sub filter {
362 0     0 1   my ( $self, $field, $operator, $value ) = @_;
363             return {
364 0           property_name => $field,
365             operator => $operator,
366             property_value => $value
367             };
368             }
369              
370             =head2 error_message()
371              
372             Returns the detailed explanation of the last error. Empty string if
373             everything went fine.
374              
375             my $res = $keen->put('in_out_log', $data);
376             unless ($res) {
377             warn 'Something went wrong '.$keen->error_message();
378             }
379              
380             =cut
381              
382             =head1 AUTHOR
383              
384             Andrew Zhilenko, C<< >>
385             (c) Putin Huylo LLC, 2015
386              
387             =head1 BUGS
388              
389             Please report any bugs or feature requests to C, or through
390             the web interface at L.
391             I will be notified, and then you'll automatically be notified of progress on your bug as I make changes.
392              
393             =head1 SUPPORT
394              
395             You can find documentation for this module with the perldoc command.
396              
397             perldoc WWW::KeenIO
398              
399              
400             You can also look for information at:
401              
402             =over 4
403              
404             =item * RT: CPAN's request tracker (report bugs here)
405              
406             L
407              
408             =item * AnnoCPAN: Annotated CPAN documentation
409              
410             L
411              
412             =item * CPAN Ratings
413              
414             L
415              
416             =item * Search CPAN
417              
418             L
419              
420             =back
421              
422             =head1 LICENSE AND COPYRIGHT
423              
424             Copyright 2015 Putin Huylo LLC
425              
426             This program is free software; you can redistribute it and/or modify it
427             under the terms of the the Artistic License (2.0). You may obtain a
428             copy of the full license at:
429              
430             L
431              
432             Any use, modification, and distribution of the Standard or Modified
433             Versions is governed by this Artistic License. By using, modifying or
434             distributing the Package, you accept this license. Do not use, modify,
435             or distribute the Package, if you do not accept this license.
436              
437             If your Modified Version has been derived from a Modified Version made
438             by someone other than you, you are nevertheless required to ensure that
439             your Modified Version complies with the requirements of this license.
440              
441             This license does not grant you the right to use any trademark, service
442             mark, tradename, or logo of the Copyright Holder.
443              
444             This license includes the non-exclusive, worldwide, free-of-charge
445             patent license to make, have made, use, offer to sell, sell, import and
446             otherwise transfer the Package with respect to any patent claims
447             licensable by the Copyright Holder that are necessarily infringed by the
448             Package. If you institute patent litigation (including a cross-claim or
449             counterclaim) against any party alleging that the Package constitutes
450             direct or contributory patent infringement, then this Artistic License
451             to you shall terminate on the date that such litigation is filed.
452              
453             Disclaimer of Warranty: THE PACKAGE IS PROVIDED BY THE COPYRIGHT HOLDER
454             AND CONTRIBUTORS "AS IS' AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES.
455             THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
456             PURPOSE, OR NON-INFRINGEMENT ARE DISCLAIMED TO THE EXTENT PERMITTED BY
457             YOUR LOCAL LAW. UNLESS REQUIRED BY LAW, NO COPYRIGHT HOLDER OR
458             CONTRIBUTOR WILL BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, OR
459             CONSEQUENTIAL DAMAGES ARISING IN ANY WAY OUT OF THE USE OF THE PACKAGE,
460             EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
461              
462              
463             =cut
464              
465             __PACKAGE__->meta->make_immutable;
466              
467             1; # End of WWW::KeenIO