sapis 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,241 @@
1
+ =begin
2
+
3
+ <generic_helper.rb> - Part of Sav's APIs.
4
+ Copyright (C) 2011 Saverio Miroddi
5
+
6
+ This program is free software: you can redistribute it and/or modify
7
+ it under the terms of the GNU General Public License as published by
8
+ the Free Software Foundation, either version 3 of the License, or
9
+ (at your option) any later version.
10
+
11
+ This program is distributed in the hope that it will be useful,
12
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ GNU General Public License for more details.
15
+
16
+ You should have received a copy of the GNU General Public License
17
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
18
+
19
+ =end
20
+
21
+ require 'date'
22
+
23
+ module GenericHelper
24
+ def self.do_retry(options={}, &block)
25
+ max_retries = options[:max_retries] || 3
26
+ sleep_interval = options[:sleep ] || 0
27
+
28
+ current_retries = 0
29
+
30
+ begin
31
+ yield
32
+ rescue
33
+ current_retries += 1
34
+
35
+ if current_retries <= max_retries
36
+ sleep sleep_interval
37
+ retry
38
+ end
39
+
40
+ raise
41
+ end
42
+ end
43
+
44
+ # Trivial version. Can easily be done with a regex.
45
+ #
46
+ def self.camelize(string)
47
+ buffer = ""
48
+ capitalize_next = true
49
+
50
+ string.chars.each do | char |
51
+ if char == '_'
52
+ capitalize_next = true
53
+ elsif capitalize_next
54
+ buffer << char.upcase
55
+ capitalize_next = false
56
+ else
57
+ buffer << char
58
+ end
59
+ end
60
+
61
+ buffer
62
+ end
63
+
64
+ # options:
65
+ #
66
+ # :past: [false] the date can be future (supported only by some cases)
67
+ #
68
+ # Format:
69
+ #
70
+ # 'YYYY-MM-DD', 'YYYYMMDD', 'MMM/DD/YYYY', 'MMM/D(D)', 'D(D)/MMM' (curr. year), 'MMDD' (curr. year)
71
+ # 'to[day]', 'ye[sterday]', 'mon' (monday), 'tue-' (last tuesday),
72
+ # '+3' (today+3), '-2' (today-2)
73
+ #
74
+ # Note that the easiest way to pass '-<value>' in bash, is to use '--' (end of options).
75
+ #
76
+ def decode_date(encoded_date, future: true)
77
+ case encoded_date.downcase
78
+ # YYYY-MM-DD
79
+ when %r{^(\d\d\d\d)-(\d\d)-(\d\d)$}
80
+ Date.new($1.to_i, $2.to_i, $3.to_i)
81
+ # YYYYMMDD
82
+ when /^(\d\d\d\d)(\d\d)(\d\d)$/
83
+ Date.new($1.to_i, $2.to_i, $3.to_i)
84
+ # MMM/DD/YYYY
85
+ when %r{^(\w{3})/(\d\d)/(\d\d\d\d)$}
86
+ month_index_for_time = Date.strptime($1, "%b").month
87
+ Date.new($3.to_i, month_index_for_time, $2.to_i)
88
+ # MMM/D(D) (+future)
89
+ when %r{^(\w{3})\/(\d{1,2})$}
90
+ month_index_for_time = Date.strptime($1, "%b").month
91
+ Date.new(Date.today.year, month_index_for_time, $2.to_i)
92
+ .then { !future && it > Date.today ? it << 12 : it }
93
+ # D(D)/MMM/YYYY
94
+ when %r{^(\d{1,2})/(\w{3})/(\d\d\d\d)$}
95
+ month_index_for_time = Date.strptime($2, "%b").month
96
+ Date.new($3.to_i, month_index_for_time, $1.to_i)
97
+ # D(D)/MMM
98
+ when %r{^(\d{1,2})\/(\w{3})$}
99
+ month_index_for_time = Date.strptime($2, "%b").month
100
+ Date.new(Date.today.year, month_index_for_time, $1.to_i)
101
+ # MMDD
102
+ when /^(\d\d)(\d\d)$/
103
+ Date.new(Time.now.year, $1.to_i, $2.to_i)
104
+ when 'to', 'today'
105
+ Date.today
106
+ when 'ye', 'yesterday'
107
+ Date.today - 1
108
+ when /^(sun|mon|tue|wed|thu|fri|sat)$/
109
+ diff = (Date.strptime($1, "%a") - Date.today).to_i
110
+ diff += 7 if diff <= 0
111
+ Date.today + diff
112
+ when /^(sun|mon|tue|wed|thu|fri|sat)-$/
113
+ diff = (Date.strptime($1, "%a") - Date.today).to_i
114
+ diff -= 7 if diff >= 0
115
+ Date.today + diff
116
+ when /^\+(\d+)$/
117
+ Date.today + $1.to_i
118
+ when /^-(\d+)$/
119
+ Date.today - $1.to_i
120
+ else
121
+ raise "Unrecognized date: #{ encoded_date }"
122
+ end
123
+ end
124
+
125
+ # Params:
126
+ #
127
+ # day, start, end
128
+ #
129
+ # Format:
130
+ #
131
+ # * DAY: 'YYYY-MM-DD', 'YYYYMMDD', 'MMDD', 'to[day]', 'ye[sterday]', 'thu' (thursday of the current week), 'mon-' (last monday), 'tue+' (next tuesday), 'MMM/DD'
132
+ # * START: 'HH', 'HH:MM'
133
+ # * END: '3d', '2h', '45m', 'HH', 'HH:MM'
134
+ #
135
+ # Possible combinations:
136
+ #
137
+ # * (nothing) all today
138
+ # * DAY on the day, for all day
139
+ # * DAY,END from day to end, for all day
140
+ # * DAY,START,END on the day, from start to end
141
+ # * START,END today, from start to end
142
+ #
143
+ # Note that the 'current week' starts on Sunday.
144
+ #==
145
+ # In the regexes, numbered repetitions ('{n}') are not used for consistency.
146
+ #
147
+ # The Date class pretty much sucks. Between the other things, it doesn't accept string
148
+ # values when instantiating.
149
+ #
150
+ def decode_interval(*daytime)
151
+ current_token = daytime.shift
152
+
153
+ if current_token
154
+ base_day = decode_date(current_token)
155
+
156
+ if base_day.nil?
157
+ base_day = Date.today
158
+ daytime.unshift(current_token)
159
+ end
160
+ else
161
+ base_day = Date.today
162
+ daytime.unshift(current_token)
163
+ end
164
+
165
+ raise "Wrong start year format. Non-consumed tokens: #{ daytime }" if base_day.nil?
166
+
167
+ current_token = daytime.shift
168
+
169
+ # The next token could be both a start (e.g. DAY,START,END) or an END (e.g. DAY,END).
170
+ # Since we can't rely on character patterns because of the HH:MM case, we rely on the
171
+ # number of tokens: START can be present only if at this point there are two tokens.
172
+ #
173
+ if daytime.size == 1
174
+ case current_token
175
+ when /^(\d{1,2})$/
176
+ start_daytime = Time.local(base_day.year, base_day.month, base_day.day, $1)
177
+ when /^(\d{1,2}):(\d\d)$/
178
+ start_daytime = Time.local(base_day.year, base_day.month, base_day.day, $1, $2)
179
+ else
180
+ daytime.unshift(current_token)
181
+ end
182
+
183
+ all_day = false
184
+ else
185
+ start_daytime = Time.local(base_day.year, base_day.month, base_day.day)
186
+ all_day = true
187
+ daytime.unshift(current_token)
188
+ end
189
+
190
+ raise "Wrong start daytime format. Non-consumed tokens: #{ daytime }" if start_daytime.nil?
191
+
192
+ current_token = daytime.shift
193
+
194
+ if current_token
195
+ if all_day
196
+ # Only full days allowed in case of all-day timespan
197
+ #
198
+ if current_token =~ /^(\d+)d$/
199
+ end_daytime = add_days(start_daytime, $1.to_i)
200
+ else
201
+ daytime.unshift(current_token)
202
+ end
203
+ else
204
+ case current_token.downcase
205
+ when /^(\d+)d$/
206
+ end_daytime = start_daytime + $1.to_i * 24 * 60 * 60
207
+ when /^(\d+)h$/
208
+ end_daytime = start_daytime + $1.to_i * 60 * 60
209
+ when /^(\d+)m$/
210
+ end_daytime = start_daytime + $1.to_i * 60
211
+ when /^(\d{1,2})$/
212
+ end_daytime = Time.local(start_daytime.year, start_daytime.month, start_daytime.day, $1)
213
+ when /^(\d{1,2}):(\d\d)$/
214
+ end_daytime = Time.local(start_daytime.year, start_daytime.month, start_daytime.day, $1, $2)
215
+ else
216
+ daytime.unshift(current_token)
217
+ end
218
+ end
219
+ else
220
+ # We're here if none of START/END have been passed; we default to +1 day.
221
+ # At this point, there are no tokens to unshift
222
+ #
223
+ end_daytime = add_days(start_daytime, 1)
224
+ end
225
+
226
+ raise "Wrong end daytime format. Non-consumed tokens: #{ daytime }" if end_daytime.nil?
227
+
228
+ raise "Non-consumed tokens found: #{ daytime }" if daytime.size > 0
229
+
230
+ [start_daytime, end_daytime, all_day]
231
+ end
232
+
233
+ private
234
+
235
+ # Add the given days, ignoring the daylight saving.
236
+ #
237
+ def add_days(base_time, days)
238
+ result_date = Date.new(base_time.year, base_time.month, base_time.day) + days
239
+ Time.local(result_date.year, result_date.month, result_date.day)
240
+ end
241
+ end
@@ -0,0 +1,36 @@
1
+ =begin
2
+
3
+ <gnome_helper.rb> - Part of Sav's APIs.
4
+ Copyright (C) 2011 Saverio Miroddi
5
+
6
+ This program is free software: you can redistribute it and/or modify
7
+ it under the terms of the GNU General Public License as published by
8
+ the Free Software Foundation, either version 3 of the License, or
9
+ (at your option) any later version.
10
+
11
+ This program is distributed in the hope that it will be useful,
12
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ GNU General Public License for more details.
15
+
16
+ You should have received a copy of the GNU General Public License
17
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
18
+
19
+ =end
20
+
21
+ require_relative 'bash_helper'
22
+
23
+ module GnomeHelper
24
+
25
+ include BashHelper
26
+
27
+ def set_gnome_background( pic_path )
28
+ simple_bash_execute "gconftool -t string -s /desktop/gnome/background/picture_filename", pic_path
29
+ end
30
+
31
+ def get_gnome_background_filename
32
+ simple_bash_execute "gconftool -g /desktop/gnome/background/picture_filename"
33
+ end
34
+
35
+ end
36
+
@@ -0,0 +1,202 @@
1
+ =begin
2
+
3
+ <graphing_helper.rb> - Part of Sav's APIs.
4
+ Copyright (C) 2011 Saverio Miroddi
5
+
6
+ This program is free software: you can redistribute it and/or modify
7
+ it under the terms of the GNU General Public License as published by
8
+ the Free Software Foundation, either version 3 of the License, or
9
+ (at your option) any later version.
10
+
11
+ This program is distributed in the hope that it will be useful,
12
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ GNU General Public License for more details.
15
+
16
+ You should have received a copy of the GNU General Public License
17
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
18
+
19
+ =end
20
+
21
+ module GraphingHelper
22
+
23
+ require 'gruff'
24
+ require 'tempfile'
25
+
26
+ # Transpose from format:
27
+ #
28
+ # label1, labelN, <ignored>
29
+ # value, value, day1
30
+ # value, value, day2
31
+ #
32
+ # <source_data> is destroyed.
33
+ # Labels must be unique.
34
+ # Days can be either a string or a Date.
35
+ #
36
+ def self.transpose_data_top_headers( source_data, options={} )
37
+ fill_missing_days = ! options.has_key?( :fill_missing_days ) || !! options[ :fill_missing_days ]
38
+
39
+ labels = source_data.shift[ 0...-1 ]
40
+
41
+ source_data.each { | row | row[ row.size - 1 ] = Date.strptime( row.last ) unless row.last.is_a?( Date ) }
42
+
43
+ min_day = source_data.map { | row | row.last }.min
44
+ max_day = source_data.map { | row | row.last }.max
45
+
46
+ # Pre-fill the output matrix (hash)
47
+
48
+ output_data = labels.inject( {} ) do | current_output_data, label |
49
+ current_output_data[ label ] = [ nil ] * ( max_day - min_day )
50
+ current_output_data
51
+ end
52
+
53
+ # Fill the values
54
+
55
+ source_data.each do | row |
56
+ day = row.last
57
+
58
+ labels.each_with_index do | label, i |
59
+ output_data[ label ][ day - min_day ] = row[ i ]
60
+ end
61
+ end
62
+
63
+ # Apply final manipulations and convert output data to array
64
+
65
+ output_data.each { | label, values | values.compact! } if ! fill_missing_days
66
+
67
+ output_data = output_data.map { | label, values | [ label, values ] }
68
+ days = ( min_day..max_day ).to_a
69
+
70
+ [ output_data, days ]
71
+ end
72
+
73
+ # Transpose from format:
74
+ #
75
+ # label1, value, day1
76
+ # labelM, value, day2
77
+ # label1, value, day3
78
+ # labelN, value, day3
79
+ #
80
+ def self.transpose_data_left_headers( source_data )
81
+ source_data.each { | row | row[ row.size - 1 ] = Date.strptime( row.last ) unless row.last.is_a?( Date ) }
82
+
83
+ # Fill a map day => { label => value, ... }
84
+
85
+ data_by_day_by_label = {}
86
+
87
+ source_data.each do | label, value, day |
88
+ day = Date.strptime( day ) unless day.is_a?( Date )
89
+
90
+ data_by_day_by_label[ day ] ||= {}
91
+ data_by_day_by_label[ day ][ label ] = value
92
+ end
93
+
94
+ # Convert to tabular form by label
95
+
96
+ labels = source_data.map { | row | row.first }.uniq
97
+
98
+ output_data = labels.inject( {} ) do | current_output_data, label |
99
+ current_output_data[ label ] = []
100
+ current_output_data
101
+ end
102
+
103
+ days = data_by_day_by_label.keys.sort
104
+
105
+ days.each do | day |
106
+ labels_values = data_by_day_by_label[ day ]
107
+
108
+ labels.each do | label |
109
+ value = labels_values[ label ]
110
+ output_data[ label ] << value
111
+ end
112
+ end
113
+
114
+ # Apply final manipulations and convert output data to array
115
+
116
+ output_data = output_data.map { | label, values | [ label, values ] }
117
+
118
+ [ output_data, days ]
119
+ end
120
+
121
+ # Ouput a line graph, optionally to a file.
122
+ #
123
+ # Assumes that the row contains the headers.
124
+ #
125
+ # options:
126
+ # :out_file output file. if not passed, the graph is displayed live
127
+ #
128
+ def self.format_as_line_graph( data, days, options={} )
129
+ out_file = options[ :out_file ]
130
+
131
+ graph = Gruff::Line.new
132
+
133
+ data.each { | label_data | graph.data( *label_data ) }
134
+
135
+ graph.labels = {
136
+ 0 => days.first.to_s,
137
+ days.size - 1 => days.last.to_s,
138
+ }
139
+
140
+ if out_file
141
+ graph.write( out_file )
142
+ else
143
+ Tempfile.open( 'tracking_graph' ) do | f |
144
+ # Base#write doesn't work because the tempfile doesn't have any extension
145
+ #
146
+ rendered_data = graph.to_blob
147
+ f << rendered_data
148
+
149
+ images_display_app = get_images_display_app
150
+ `#{ images_display_app } #{ f.path }`
151
+ end
152
+ end
153
+ end
154
+
155
+ # Assumes that the number of fields for each row is constant.
156
+ #
157
+ # options:
158
+ # :separator default: '|'
159
+ # :align hash { <field> => :left }. makes sense only if the first row is the headers.
160
+ #
161
+ def self.format_as_table( rows, options={} )
162
+ return "" if rows.empty?
163
+
164
+ separator = options[ :separator ] || '|'
165
+ align = options[ :align ] || {}
166
+
167
+ max_field_sizes = nil
168
+ alignment_symbols = nil
169
+
170
+ rows.each_with_index do | row, row_num |
171
+ if row_num == 0
172
+ max_field_sizes = row.map { | value | value.to_s.size }
173
+ alignment_symbols = row.map { | value | '-' if align[ value ] == :left }
174
+ else
175
+ row.each_with_index do | value, value_pos |
176
+ max_field_sizes[ value_pos ] = value.to_s.size if value.to_s.size > max_field_sizes[ value_pos ]
177
+ end
178
+ end
179
+ end
180
+
181
+ template = separator + max_field_sizes.zip( alignment_symbols ).map { | size, alignment_symbol | " %#{ alignment_symbol }#{ size }s #{ separator }" }.join
182
+
183
+ rows.inject( "" ) do | buffer, row |
184
+ buffer << template % row << "\n"
185
+ end
186
+ end
187
+
188
+ private
189
+
190
+ def self.get_images_display_app
191
+ case RUBY_PLATFORM
192
+ when /linux/
193
+ 'eog'
194
+ when /darwin/
195
+ 'open -W'
196
+ else
197
+ raise "Unsupported platform: #{ RUBY_PLATFORM }"
198
+ end
199
+ end
200
+
201
+ end
202
+
@@ -0,0 +1,148 @@
1
+ =begin
2
+
3
+ <interactions_helper.rb> - Part of Sav's APIs.
4
+ Copyright (C) 2011 Saverio Miroddi
5
+
6
+ This program is free software: you can redistribute it and/or modify
7
+ it under the terms of the GNU General Public License as published by
8
+ the Free Software Foundation, either version 3 of the License, or
9
+ (at your option) any later version.
10
+
11
+ This program is distributed in the hope that it will be useful,
12
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
13
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
+ GNU General Public License for more details.
15
+
16
+ You should have received a copy of the GNU General Public License
17
+ along with this program. If not, see <http://www.gnu.org/licenses/>.
18
+
19
+ =end
20
+
21
+ module InteractionsHelper
22
+
23
+ require 'highline/import'
24
+
25
+ def self.secure_ask( question='Insert password: ' )
26
+ HighLine.new.ask( question ) { | q | q.echo = '*' }
27
+ end
28
+
29
+ # Asks a question, optionally using a default.
30
+ #
31
+ # Format:
32
+ #
33
+ # <header> [default]?
34
+ #
35
+ def self.ask_entry( header, default=nil )
36
+ while true
37
+ print "#{ header }"
38
+ print " [#{ default }]" if default
39
+ print "? "
40
+
41
+ answer = STDIN.gets.chomp
42
+
43
+ if answer = '' && default
44
+ return default
45
+ elsif answer != ''
46
+ return answer
47
+ end
48
+ end
49
+ end
50
+
51
+ # Displays a numbered (or user-choosen) list of entries, in the format:
52
+ #
53
+ # entry_a) value_a
54
+ # entry_b) value_b
55
+ #
56
+ # or
57
+ #
58
+ # 0) value_a
59
+ # 1) value_b
60
+ #
61
+ # depending on <entries> being respectively a Hash or an Array.
62
+ #
63
+ # options:
64
+ # :default: default entry, if the user doesn't choose
65
+ # :autochoose_if_one: [false] automatically choose the entry if it's only one
66
+ # :filter_by: [nil] String (for simplicity), which is matched case-insensitively.
67
+ # If there are more matches and the pattern matches exactly one of
68
+ # them, it's automatically chosen.
69
+ #
70
+ def self.ask_entries_with_points( header, entries, options={} )
71
+ raise ArgumentError.new("No entries passed! [#{header}]") if entries.empty?
72
+
73
+ default = options[ :default ]
74
+ autochoose_if_one = options[ :autochoose_if_one ]
75
+ filtering_pattern = options[ :filter_by ]
76
+
77
+ raise "Pattern must be a String, Regexp is not supported" if filtering_pattern.is_a?( Regexp )
78
+
79
+ # Convert to Hash if it's an array
80
+ #
81
+ if entries.is_a?( Array )
82
+ entries = ( 0 ... entries.size ).zip( entries )
83
+
84
+ entries = entries.inject( {} ) do | current_entries, ( i, entry ) |
85
+ current_entries[ i.to_s ] = entry
86
+ current_entries
87
+ end
88
+ end
89
+
90
+ if filtering_pattern
91
+ exact_matches = entries.select { | _, entry_value | entry_value.downcase == filtering_pattern.downcase }
92
+
93
+ return exact_matches.values.first if exact_matches.size == 1
94
+
95
+ entries = entries.select { | _, entry_value | entry_value.downcase.include?( filtering_pattern.downcase ) }
96
+
97
+ raise ArgumentError.new("No entries after filtering! [#{header}, #{filtering_pattern}]") if entries.empty?
98
+ end
99
+
100
+ if entries.size == 1 && (autochoose_if_one || filtering_pattern)
101
+ return entries.values.first
102
+ end
103
+
104
+ while true
105
+ puts "#{ header }:"
106
+
107
+ entries.each do | point, entry |
108
+ print " #{ point }"
109
+ print default.to_s == entry ? '*' : ')'
110
+ puts " #{ entry }"
111
+ end
112
+
113
+ answer = STDIN.gets.chomp
114
+
115
+ if answer == '' && default
116
+ break default
117
+ elsif entries.has_key?( answer )
118
+ break entries[ answer ]
119
+ end
120
+ end
121
+ end
122
+
123
+ # Displays a list of entries, in the format:
124
+ #
125
+ # header: entry_a,entry_b [default]?
126
+ #
127
+ def self.ask_entries_in_line( header, entries, default=nil )
128
+ while true
129
+ print "#{ header }: "
130
+
131
+ print entries.join( ',' )
132
+
133
+ print " [#{ default }]" if default
134
+
135
+ print "? "
136
+
137
+ answer = STDIN.gets.chomp
138
+
139
+ if answer == '' && default
140
+ break default
141
+ elsif entries.include?( answer )
142
+ break answer
143
+ end
144
+ end
145
+ end
146
+
147
+ end
148
+