swivel 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (7) hide show
  1. data/CHANGELOG +4 -0
  2. data/COPYING +18 -0
  3. data/README +315 -0
  4. data/Rakefile +80 -0
  5. data/bin/swivel +135 -0
  6. data/lib/swivel.rb +464 -0
  7. metadata +58 -0
data/CHANGELOG ADDED
@@ -0,0 +1,4 @@
1
+ = 0.0.1
2
+ == 4th June, 2007
3
+
4
+ * An api for swivel. XML ensues.
data/COPYING ADDED
@@ -0,0 +1,18 @@
1
+ Copyright (c) 2007 Swivel
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to
5
+ deal in the Software without restriction, including without limitation the
6
+ rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
7
+ sell copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
16
+ THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
17
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
18
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README ADDED
@@ -0,0 +1,315 @@
1
+ = swivel.rb
2
+
3
+ Without fancy screencast software, I'm content with this:
4
+
5
+ ##### # # ### # # ####### # ###### ######
6
+ # # # # # # # # # # # # # #
7
+ # # # # # # # # # # # # #
8
+ ##### # # # # # # ##### # ###### ######
9
+ # # # # # # # # # ### # # # #
10
+ # # # # # # # # # # ### # # # #
11
+ ##### ## ## ### # ####### ####### ### # # ######
12
+
13
+ (bam!)
14
+
15
+ swivel.rb is a smallish bit that lets you interface with Swivel's REST API.
16
+
17
+ == The REST API
18
+
19
+ TODO: some copy
20
+
21
+ ------ ----------- ------------
22
+ method uri what it does
23
+ ------ ----------- ------------
24
+
25
+ data_columns
26
+ get /rest/data_columns list data_columns
27
+ get /rest/data_columns/#{id} show data_column #{id}
28
+ put /rest/data_columns/#{id} update data_column #{id}
29
+
30
+ data_sets
31
+ post /rest/data_sets create a new data_set
32
+ delete /rest/data_sets/#{id} delete data_set #{id}
33
+ get /rest/data_sets list data_sets
34
+ get /rest/data_sets/#{id} show data_set #{id}
35
+ put /rest/data_sets/#{id} update data_set #{id}
36
+
37
+ graphs
38
+ post /rest/graphs create a new graph
39
+ delete /rest/graphs/#{id} delete graph #{id}
40
+ get /rest/graphs list graphs
41
+ get /rest/graphs/#{id} show graph #{id}
42
+ put /rest/graphs/#{id} update graph #{id}
43
+
44
+ users
45
+ get /rest/users list users
46
+ get /rest/users/#{id} show user #{id}
47
+ put /rest/users/#{id} update user #{id}
48
+
49
+ TODO: optional parameters for lists, etc.
50
+ TODO: search
51
+ TODO: echo
52
+
53
+ == Make it go go racer
54
+
55
+ $ sudo gem install swivel
56
+ Successfully installed swivel, version 0.0.1
57
+ Installing ri documentation for swivel-0.0.1...
58
+ Installing RDoc documentation for swivel-0.0.1...
59
+
60
+ $ irb
61
+ >> require 'swivel'
62
+ => true
63
+ >> swivel = Swivel::Connection.new
64
+ => #<Swivel::Connection:0xb7a29fb0 ...
65
+ >> puts swivel.call('/rest/test/echo/howdy')
66
+ <?xml version="1.0" encoding="UTF-8"?>
67
+ <response at="Sat, 09 Jun 2007 23:59:29 -0700" success="true">
68
+ <echo version="0" text="howdy"/>
69
+ </response>
70
+ => nil
71
+
72
+ == Class hierarchy
73
+
74
+ * Swivel::Connection
75
+ * Swivel::Connection::Config
76
+ * Swivel::Response
77
+ * Swivel::DataColumn
78
+ * Swivel::DataSet
79
+ * Swivel::Graph
80
+ * Swivel::User
81
+ * Swivel::List
82
+ * Swivel::ApiError
83
+
84
+ == RTF!M
85
+
86
+ Never fear, it's installed when you install the gem.
87
+
88
+ $ ri Swivel
89
+
90
+ Another nice one:
91
+
92
+ $ ri Swivel::Response
93
+
94
+ == Examples. Worth a thousand words
95
+
96
+ Ready, set, ... wait. Got an API key? Get an API key (http://swivel.com/api/key).
97
+ Try to keep your API key close to your chest, safely tucked away from prying eyes
98
+ and dangers of the "real world"... dangers like parking tickets and untied shoelaces.
99
+ It's like a password, so don't share it.
100
+
101
+ # writes your api key into ~/.swivelrc
102
+ swivel -k <your api key>
103
+
104
+ Now, back to it. Ready, set, go!
105
+
106
+ $ irb
107
+ >> require 'swivel'
108
+ => true
109
+
110
+ A Swivel::Connection instance is your main interface to Swivel's API. The
111
+ connection loads up (and possibly creates) your ~/.swivelrc and sets up
112
+ several parameters, such as your API key, that are used throughout calls to
113
+ Swivel.
114
+
115
+ >> swivel = Swivel::Connection.new
116
+ => #<Swivel::Connection:0xb7a2bb58 @config={:timeout_down=>10, :api_key=>"xxx", :host=>"api.swivel.com", :port=>80, :timeout_up=>200}, headers{"Accept"=>"application/xml"}
117
+
118
+ Swivel::Connection#call is the method you'll probably use most frequently.
119
+ Send in any REST url (including query strings containing any options) and
120
+ it will try faithfully to get back something useful for you.
121
+
122
+ In many cases, Swivel::Connection#call will return an object whose class is
123
+ inherited from Swivel::Response.
124
+
125
+ # look! we got a Swivel::DataSet object! frabjous day!
126
+ >> data_set = swivel.call '/rest/data_sets/1000000'
127
+ => #<Swivel::DataSet:0xb79dd688 @response=<response success='true' at='Mon, 04 Jun 2007 04:41:04 -0700'> .... , xml_tag"data-set", docUNDEFINED ....
128
+
129
+ Question: what if it can't find an appropriate class to instantiate? Then it
130
+ just gives you back the XML as a String, trusting that you'll love and care for
131
+ it.
132
+
133
+ # Swivel::Connection#call can't find a class to instantiate this time,
134
+ # so it just sends us XML.
135
+ >> puts swivel.call('/rest/test/echo/howdy')
136
+ <?xml version="1.0" encoding="UTF-8"?>
137
+ <response success="true" at="Mon, 04 Jun 2007 03:34:49 -0700">
138
+ <echo text="howdy" version="0"/>
139
+ </response>
140
+
141
+ But, back to objects. Swivel::Connection#call often returns objects that
142
+ encapsulate the XML response that the Swivel API sent.
143
+
144
+ >> data_set = swivel.call '/rest/data_sets/1000000'
145
+ => #<Swivel::DataSet:0xb79dd688 @response=<response success='true' at='Mon, 04 Jun 2007 04:41:04 -0700'> .... , xml_tag"data-set", docUNDEFINED ....
146
+
147
+ These objects are rich and meaty. You can poke them and they shall respond,
148
+ surlily.
149
+
150
+ >> data_set.id
151
+ => 1000000
152
+ >> data_set.user.name
153
+ => "huned"
154
+ >> data_set.data_columns[3].name
155
+ => "by-nc-nd-2.0"
156
+
157
+ However, these objects are magickal in the ruby way, and they wish to often
158
+ tightly conceal their secrets. Some standard snooping shall leave you unsatisfied:
159
+
160
+ # let's call Swivel::DataSet#id
161
+ >> data_set.swivel_id
162
+ => 1000000 # huzzah!
163
+ # yet... it's not there in the list of methods
164
+ >> data_set.methods.grep /swivel_id/
165
+ => [] # what the..?
166
+
167
+ So how do you know what to call? At this time, the best way is to inspect
168
+ the inner power-juice, the XML.
169
+
170
+ >> puts data_set.to_xml
171
+ <data-set swivel-id='1000000' version='0'>
172
+ <name>name?</name>
173
+ <user swivel-id='1000010'>
174
+ <name>huned</name>
175
+ </user>
176
+ <created-at>Sat, 02 Jun 2007 20:35:45 -0700</created-at>
177
+ <updated-at>Sat, 02 Jun 2007 20:35:49 -0700</updated-at>
178
+ <source>
179
+ <citation>name?</citation>
180
+ <citation-url/>
181
+ </source>
182
+ <rows>367</rows>
183
+ <columns>7</columns>
184
+ ...
185
+ </data-set>
186
+
187
+ == Constructing URLs
188
+
189
+ Constructing URLs are free and easy! (Freeasy... mmm!)
190
+
191
+ Any object in Swivel has a corresponding page that you can view with your
192
+ browser.
193
+
194
+ # get the data_set's id
195
+ id = data_set.id
196
+ # get the data_set's resource... turns Swivel::DataSet into 'data_set'
197
+ resource = data_set.class.name.split('::').last.underscore # => "data_set"
198
+
199
+ # copy/paste this url into your browser
200
+ url = "http://swivel.com/#{resource}s/show/#{id}"
201
+
202
+ Graphs have pages, but if you want to grab the actual graph image, here's how:
203
+
204
+ # as before, we get the id and resource type
205
+ id = data_set.id
206
+ resource = data_set.class.name.split('::').last.underscore # => "data_set"
207
+
208
+ url = "http://swivel.com/#{resource}s/image/share/#{width}/#{height}"
209
+
210
+ If you have a specific graph visualization in mind, you can use a complex-er URL that allows you to decorate the image with various options like time scales, aggregation functions, and graph types.
211
+
212
+ url = "http://swivel.com/#{resource}s/image/share/#{width}/#{height}/#{limit}/#{scale}/#{graph_type}/#{order_by_direction}/#{time_range}/#{time_scale}/#{aggregation_function}"
213
+
214
+ TODO: explain it in this here table
215
+
216
+ * width
217
+ * height
218
+ * limit
219
+ * scale
220
+ * graph_type
221
+ * order_by_direction
222
+ * time_range
223
+ * time_scale
224
+ * aggregation_function
225
+
226
+ == Something completely different: A tryst at the command line
227
+
228
+ The command line program lets you query Swivel or upload data into Swivel.
229
+ You get this program when you install the Swivel gem.
230
+
231
+ $ which swivel
232
+ /usr/bin/swivel
233
+
234
+ It uses a ~/.swivelrc file to remember settings. If you don't have a
235
+ ~/.swivelrc, running the program will create a default one for you.
236
+
237
+ $ ls -lh ~/.swivelrc
238
+ ls: /home/huned/.swivelrc: No such file or directory
239
+ $ swivel
240
+ Usage: swivel [options]
241
+ -h, --host=name Swivel hostname or IP address.
242
+ Default:
243
+ -p, --port=number Swivel host's port number.
244
+ Default:
245
+ -f, --file=file File to upload, append, or replace.
246
+ -r, --raw=path Perform a raw call and print the XML response.
247
+ -?, --help Show this help message.
248
+ -k, --key=api-key Set your API key.
249
+ $ ls -lh ~/.swivelrc
250
+ -rw-rw-r-- 1 huned huned 105 Jun 4 05:04 /home/huned/.swivelrc
251
+
252
+ Note, however, that the api_key setting is blank. Once you finagle an api key,
253
+ run `swivel -k <your api key>` to update your ~/.swivelrc. (Remember: finagle an api
254
+ key from http://swivel.com/api/key.)
255
+
256
+ $ cat ~/.swivelrc
257
+ ---
258
+ protocol: http://
259
+ timeout_up: 200
260
+ api_key: ""
261
+ timeout_down: 100
262
+ host: api.swivel.com
263
+ port: 80
264
+
265
+ So how about uploading data?
266
+
267
+ $ swivel upload "my awesome data" -f data.csv
268
+ uploaded data_set 1000234
269
+
270
+ Then your dataset magickally appears online at:
271
+
272
+ http://swivel.com/data_sets/show/1000234
273
+
274
+ Here's the same thing, but via STDIN. Consuming data from STDIN is a powerful
275
+ little mechanism that lets you rig arbitrary swivel uploads through unix
276
+ process piping (|).
277
+
278
+ $ cat data.csv | swivel upload "my awesome data"
279
+ uploaded data_set 1000234
280
+
281
+ If you want to append to a data_set you previously uploaded:
282
+
283
+ $ cat more_data.csv | swivel append 1000234
284
+ appended data_set 1000234
285
+
286
+ And finally, if you want to replace the entire data set with some other,
287
+ fancier data:
288
+
289
+ $ cat fancier_data.csv | swivel replace 1000234
290
+ replaced data_set 1000234
291
+
292
+ One caveat when appending or replacing: Your new data must have the same
293
+ column structure as the original data. Or in other words, your new data must
294
+ have the same column structure as the original data.
295
+
296
+ In addition to being a fine way to use swivel, the command line program
297
+ serves as a nice example of how you might use swivel.rb. swivel.rb is the
298
+ piece of code that allows ruby and the swivel api to be superhero and sidekick.
299
+ (Stop here for a moment and visualize that.)
300
+
301
+ Edit by Visnu: Huned wrote this at 4am. He tired out right here.
302
+
303
+ == Feedback
304
+
305
+ Feedback, comments, and (especially) patches welcome at developer@swivel.com.
306
+
307
+ == Respek
308
+
309
+ Respeks to _why, errtheblog, 37signals, our moms, and of course the community.
310
+ Wanna write code with us? http://swivel.com/about/jobs
311
+
312
+ == License
313
+
314
+ This software is licensed under the exact same license as Ruby itself. Peace
315
+ out.
data/Rakefile ADDED
@@ -0,0 +1,80 @@
1
+ #
2
+ # this file (graciously) adapted from hpricot. (respeks to _why.)
3
+ #
4
+
5
+ require 'rake'
6
+ require 'rake/clean'
7
+ require 'rake/gempackagetask'
8
+ require 'rake/rdoctask'
9
+ require 'rake/testtask'
10
+ require 'fileutils'
11
+ include FileUtils
12
+
13
+ NAME = "swivel"
14
+ REV = `svn info`[/Revision: (\d+)/, 1] rescue nil
15
+ VERS = ENV['VERSION'] || "0.0" + (REV ? ".#{REV}" : "")
16
+ CLEAN.include ['doc', 'pkg']
17
+ RDOC_OPTS = ['--line-numbers', '--title', 'swivel.rb', '--main', 'README', '--inline-source']
18
+
19
+ desc "Does a full compile, test run"
20
+ task :default => [:package, :test, :rdoc]
21
+
22
+ desc "Packages up Swivel."
23
+ task :package => [:clean]
24
+
25
+ desc "Releases packages for all Swivel packages and platforms."
26
+ task :release => [:package]
27
+
28
+ desc "Run all the tests"
29
+ Rake::TestTask.new do |t|
30
+ t.libs << "test"
31
+ t.test_files = FileList['test/test_*.rb']
32
+ t.verbose = true
33
+ end
34
+
35
+ Rake::RDocTask.new do |rdoc|
36
+ rdoc.rdoc_dir = 'doc/rdoc'
37
+ rdoc.options += RDOC_OPTS
38
+ rdoc.main = "README"
39
+ rdoc.rdoc_files.add ['README', 'CHANGELOG', 'COPYING', 'lib/*.rb']
40
+ end
41
+
42
+ spec =
43
+ Gem::Specification.new do |s|
44
+ s.name = NAME
45
+ s.version = VERS
46
+ s.summary = 'Ruby interface to the Swivel API.'
47
+ s.description = <<-EOS
48
+ This gem installs client library for accessing Swivel through it's API.
49
+ EOS
50
+
51
+ s.has_rdoc = true
52
+ s.rdoc_options += RDOC_OPTS
53
+ s.extra_rdoc_files = ["README", "CHANGELOG", "COPYING"]
54
+
55
+ s.author = 'huned'
56
+ s.email = 'huned@swivel.com'
57
+ s.homepage = 'http://swivel.com/developer'
58
+
59
+ s.files = %w/COPYING README Rakefile/ + Dir['{lib,bin}/*']
60
+ s.require_path = "lib"
61
+ s.bindir = "bin"
62
+ end
63
+
64
+ Rake::GemPackageTask.new(spec) do |p|
65
+ p.need_tar = true
66
+ p.gem_spec = spec
67
+ end
68
+
69
+ task "lib" do
70
+ directory "lib"
71
+ end
72
+
73
+ task :install do
74
+ sh %{rake package}
75
+ sh %{sudo gem install pkg/#{NAME}-#{VERS}}
76
+ end
77
+
78
+ task :uninstall => [:clean] do
79
+ sh %{sudo gem uninstall #{NAME}}
80
+ end
data/bin/swivel ADDED
@@ -0,0 +1,135 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'rubygems'
4
+ require 'active_support' # TODO: make it work w/o this
5
+ require 'optparse'
6
+ require File.dirname(__FILE__) + '/../lib/swivel'
7
+ require 'yaml'
8
+
9
+ #require 'ruby-debug'
10
+ #Debugger.start
11
+
12
+ options = Hash.new
13
+ config = Swivel::Connection::Config.new
14
+ config.load
15
+ config.save
16
+
17
+ ARGV.options do |opts|
18
+ opts.on '-h', '--host=name', String,
19
+ "Swivel hostname or IP address.",
20
+ "Default: #{options[:host]}" do |v| options[:host] = v end
21
+
22
+ opts.on '-p', '--port=number', String,
23
+ "Swivel host's port number.",
24
+ "Default: #{options[:port]}" do |v| options[:port] = v end
25
+
26
+ opts.on '-f', '--file=file', String,
27
+ "File to upload, append, or replace." do |v|
28
+ options[:filename] = v
29
+ end
30
+
31
+ opts.on_tail '-r', '--raw=path', String,
32
+ "Perform a raw call and print the XML response." do |v|
33
+ puts Swivel::Connection.new(options).call(v, options)
34
+ exit
35
+ end
36
+
37
+ opts.on_tail '-k', '--key=api-key',
38
+ "Set your API key." do |v|
39
+ config = Swivel::Connection::Config.new
40
+ config.config[:old_api_key] = config.config[:api_key]
41
+ config.config[:api_key] = v
42
+ config.save
43
+ exit
44
+ end
45
+
46
+ opts.on_tail '-?', '--help',
47
+ "Show this help message." do puts opts; exit end
48
+
49
+ opts.parse!
50
+
51
+ if ARGV.empty?
52
+ puts opts
53
+ exit
54
+ end
55
+ end
56
+
57
+ class SwivelHelper
58
+ def initialize options = Hash.new
59
+ @swivel = Swivel::Connection.new options
60
+ end
61
+
62
+ def show resource, id
63
+ resource = resource.pluralize
64
+ response = @swivel.call "/rest/#{resource}/#{id}"
65
+ end
66
+
67
+ def list resource, options = Hash.new
68
+ resource = resource.pluralize
69
+ response = @swivel.call "/rest/#{resource}", options
70
+ end
71
+
72
+ def upload name, options = Hash.new
73
+ filename = options[:filename]
74
+ data_set = @swivel.upload :original_asset_name => filename,
75
+ :original_asset_path => filename,
76
+ :auto_estimate => true,
77
+ :data => read(filename),
78
+ :name => name,
79
+ :citation => $0,
80
+ :display_tags => 'swivel'
81
+ puts "uploaded #{data_set.id}"
82
+ end
83
+
84
+ def append id, options = Hash.new
85
+ filename = options[:filename]
86
+ data_set = @swivel.append :id => id,
87
+ :original_asset_name => filename,
88
+ :original_asset_path => filename,
89
+ :auto_estimate => true,
90
+ :data => read(filename)
91
+ puts "appended #{data_set.id}"
92
+ end
93
+
94
+ def replace id, options = Hash.new
95
+ filename = options[:filename]
96
+ data_set = @swivel.replace :id => id,
97
+ :original_asset_name => filename,
98
+ :original_asset_path => filename,
99
+ :auto_estimate => true,
100
+ :data => read(filename)
101
+ puts "replaced #{data_set.id}"
102
+ end
103
+
104
+ private
105
+ def read filename = nil
106
+ if filename
107
+ open filename, 'r' do |f|
108
+ f.readlines.join
109
+ end
110
+ else
111
+ readlines.join
112
+ end
113
+ end
114
+ end
115
+
116
+ helper = SwivelHelper.new options
117
+ action = ARGV.shift
118
+ resource = ARGV.shift
119
+ id = ARGV.shift
120
+
121
+ case action.downcase
122
+ when 'list'
123
+ helper.list resource, options
124
+ when 'show'
125
+ helper.show resource, id
126
+ when 'upload'
127
+ name = resource
128
+ helper.upload name, options
129
+ when 'append', 'replace'
130
+ id = resource
131
+ helper.send action.to_sym, id, options
132
+ else
133
+ puts "#{action} not supported"
134
+ exit
135
+ end
data/lib/swivel.rb ADDED
@@ -0,0 +1,464 @@
1
+ require 'rubygems'
2
+ require 'active_support' # TODO: make this work w/o active_support?
3
+ require 'base64'
4
+ require 'cgi'
5
+ require 'cobravsmongoose'
6
+ require 'fileutils'
7
+ require 'net/http'
8
+ require 'rexml/document'
9
+ require 'yaml'
10
+
11
+ class String
12
+
13
+ # Returns true if the string looks numeric
14
+ # '1283.22'.numeric? # => true
15
+ # 'howdy'.numeric? # => false
16
+
17
+ def numeric?
18
+ (self =~ /^-?\d+(\.\d+|\d*)$/) != nil
19
+ end
20
+
21
+ # Returns the string with '_' translated to '-'
22
+ # 'data_set'.dashify # => "data-set"
23
+
24
+ def dashify
25
+ self.tr('_', '-')
26
+ end
27
+
28
+ # Returns the string with '-' translated to '_'
29
+ # 'data-set'.undashify # => "data_set"
30
+
31
+ def undashify
32
+ self.tr('-', '_')
33
+ end
34
+
35
+ # Returns a string as a suitable xml tag by underscoring and dashifying. Not
36
+ # perfect, but serves its purpose.
37
+ # 'DataSet'.to_xml_tag # => "data-set"
38
+
39
+ def to_xml_tag
40
+ self.underscore.dashify
41
+ end
42
+ end
43
+
44
+ class Hash
45
+
46
+ # Returns a query string generated from keys and values. CGI escaped, of course.
47
+ # { :name => 'huned', :year => 2007 }.to_query_string # => "name=huned&year=2007"
48
+
49
+ def to_query_string
50
+ keys.map do |k|
51
+ "#{CGI.escape k.to_s}=#{CGI.escape self[k].to_s}"
52
+ end.join('&')
53
+ end
54
+ end
55
+
56
+ #
57
+ # =Swivel
58
+ #
59
+ # ==Overview
60
+ #
61
+ # Create a new connection to swivel. Grabs options from ~/.swivelrc, creating
62
+ # it if necessary.
63
+ #
64
+ # swivel = Swivel::Connection.new
65
+ #
66
+ # Get data from Swivel
67
+ #
68
+ # # show a data_set's name
69
+ # data_set = swivel.call '/rest/data_sets/1000001'
70
+ # data_set.name # => "American Longevity"
71
+ #
72
+ # # list data_sets' names
73
+ # data_sets = swivel.call '/rest/data_sets'
74
+ # data_sets.collect do |data_set|
75
+ # data_set.name
76
+ # end
77
+ #
78
+ # # show a data_column's name
79
+ # data_column = swivel.call '/rest/data_columns/1000343'
80
+ # data_column.name # => "Average Per Capita Income"
81
+ #
82
+ # # list data_columns' names
83
+ # data_columns = swivel.call '/rest/data_columns'
84
+ # data_columns.collect do |data_column|
85
+ # data_column.name
86
+ # end
87
+ #
88
+ # # show a user's name
89
+ # user = swivel.call '/rest/user/1000010'
90
+ # user.name # => "huned"
91
+ #
92
+ # # list users' names
93
+ # users = swivel.call '/rest/users'
94
+ # users.collect do |user|
95
+ # user.name
96
+ # end
97
+ #
98
+ # # show a graph's name
99
+ # graph = swivel.call '/rest/graphs/5119232'
100
+ # graph.name # => "Vinyl to Ipods"
101
+ #
102
+ # # list graphs' names
103
+ # graphs = swivel.call '/rest/graphs'
104
+ # graphs.collect do |graph|
105
+ # graph.name
106
+ # end
107
+ #
108
+ # Upload data
109
+ #
110
+ # # upload a new data_set
111
+ # data_set = swivel.upload {...}
112
+ #
113
+ # # append to an existing data_set
114
+ # data_set = swivel.append {...}.merge(:id => orig_data_set_id, :mode => 'append')
115
+ #
116
+ # # replace data for an existing data_set
117
+ # data_set = swivel.append {...}.merge(:id => orig_data_set_id, :mode => 'replace')
118
+ #
119
+ # TODO: SwQL
120
+ #
121
+ # TODO: constructing URLs for csvs, html pages, etc
122
+ #
123
+ # TODO: object cache {in memory, on filesystem}
124
+ #
125
+
126
+ module Swivel
127
+
128
+ class ApiError < StandardError; end
129
+
130
+ # Encapsulates XML that Swivel returns from an API call. Generally, you'll
131
+ # never need to instantiate a Swivel::Response object. Use one of its subclasses
132
+ # instead:
133
+ #
134
+ # * Swivel::List
135
+ # * Classes defined with metaprogrammatically (and so unseen in rdoc)
136
+ # * Swivel::DataSet
137
+ # * Swivel::DataColumn
138
+ # * Swivel::Graph
139
+ # * Swivel::User
140
+ #
141
+
142
+ class Response
143
+
144
+ attr_accessor :refreshed_at
145
+
146
+ # Instantiate from XML returned from Swivel.
147
+ def initialize xml = nil, connection = nil
148
+ @connection = connection
149
+ @xml_tag = self.class.name.split('::').last.to_xml_tag
150
+ @doc = REXML::Document.new xml
151
+ if @response = REXML::XPath.first(@doc, '/response')
152
+ if error = REXML::XPath.first(@doc, '/response/error')
153
+ message = error.attribute('message').to_s
154
+ code = error.attribute('code').to_s
155
+ raise ApiError, "#{message} (#{code})"
156
+ end
157
+ # if it's a full response, strip away the outer cruft
158
+ @doc = REXML::Document.new @response.elements[1].to_s
159
+ end
160
+ end
161
+
162
+ # Most of the work in processing responses from Swivel happens here. It's
163
+ # pretty flexible in what it returns:
164
+ #
165
+ # * text from attributes
166
+ # data_set = swivel.call '/rest/data_sets/1005309'
167
+ # data_set.to_xml # => "<data-set swivel-id=\"1005309\"> ..."
168
+ #
169
+ # # invokes method_missing
170
+ # data_set.id # => 1005309
171
+ # * text from elements
172
+ # data_set = swivel.call '/rest/data_sets/1005309'
173
+ # data_set.to_xml # => "<data-set ...><name>Swivel API</name> ..."
174
+ #
175
+ # # invokes method_missing
176
+ # data_set.name # => "Swivel API"
177
+ # * objects that inherit from Swivel::Response (including Swivel::List)
178
+ # data_set = swivel.call '/rest/data_sets/1005309'
179
+ # data_set.to_xml # => "<data-set ...><user swivel-id=\"1000010\"> ..."
180
+ #
181
+ # # invokes method_missing
182
+ # data_set.user.class # => Swivel::User
183
+
184
+ def method_missing method_id
185
+ select_element = "/#{@xml_tag}/#{method_id.to_s.to_xml_tag}"
186
+ select_attribute = "/#{@xml_tag}/@#{method_id.to_s.to_xml_tag}"
187
+ select_list = "/#{@xml_tag}/list[@resource=\"#{method_id.to_s.singularize.to_xml_tag}\"]"
188
+
189
+ if el = REXML::XPath.first(@doc, select_element)
190
+ if el.attribute('swivel-id')
191
+ Response.class_for(el.name).new(el.to_s, @connection)
192
+ elsif el.has_elements?
193
+ CobraVsMongoose.xml_to_hash el.to_s
194
+ else
195
+ value_for el.text
196
+ end
197
+ elsif el = REXML::XPath.first(@doc, select_attribute)
198
+ value_for el.to_s
199
+ elsif el = REXML::XPath.first(@doc, select_list)
200
+ Swivel::List.new el.to_s, @connection
201
+ else
202
+ raise NoMethodError, "#{method_id} isn't a method of #{self.class.name}"
203
+ end
204
+ rescue Exception => e
205
+ if @retried || @refreshed_at
206
+ raise e
207
+ else
208
+ @retried = true
209
+ refresh! true
210
+ retry
211
+ end
212
+ end
213
+
214
+ # Returns the unique id in swivel for this object. Ids are unique
215
+ # for each resource.
216
+ # user = swivel.call '/rest/users/1000010'
217
+ # user.id # => 1000010
218
+ # user.id == user.swivel_id # => true
219
+
220
+ def id
221
+ swivel_id
222
+ end
223
+
224
+ # Refreshes the object's content from Swivel.
225
+ #
226
+ # data_set = swivel.call '/rest/data_sets/1005309'
227
+ # user = data_set.user
228
+ # user.refresh! # populate the object fully from Swivel
229
+
230
+ def refresh! force = false
231
+ if @connection && (force || @refreshed_at.blank?)
232
+ refreshed = @connection.call "/rest/#{@xml_tag.undashify}s/#{id}"
233
+ if refreshed.is_a? self.class
234
+ @doc = REXML::Document.new refreshed.to_xml
235
+ @refreshed_at = Time.now
236
+ end
237
+ end
238
+ self
239
+ end
240
+
241
+ # Returns the underlying XML string for this object as a string.
242
+ # user = swivel.call '/rest/users/1000010'
243
+ # puts user.to_xml
244
+
245
+ def to_xml
246
+ @doc.to_s
247
+ end
248
+
249
+ protected
250
+ # Return an appropriate Swivel::Response subclass for the given resource.
251
+ # Swivel::Response.class_for 'data-set' # => Swivel::DataSet
252
+ # Swivel::Response.class_for 'data_set' # => Swivel::DataSet
253
+ # Swivel::Response.class_for 'list' # => Swivel::List
254
+
255
+ def self.class_for resource
256
+ "Swivel::#{resource.undashify.classify}".constantize
257
+ rescue
258
+ nil
259
+ end
260
+
261
+ def value_for s #:nordoc:
262
+ s.numeric? ? s.to_i : s
263
+ end
264
+ end
265
+
266
+ %w/DataColumn DataSet Graph User/.each do |class_name|
267
+ class_eval <<-LAZY
268
+ class #{class_name} < Response; end
269
+ LAZY
270
+ end
271
+
272
+ # Encapsulates lists of resources. Typically, items contained within the list are
273
+ # subclasses of Swivel::Response.
274
+ #
275
+ # data_sets = swivel.call '/rest/data_sets'
276
+ # data_sets.class # => Swivel::List
277
+ # data_sets.collect do |d| d.class end # => [Swivel::DataSet, Swivel::DataSet, ...]
278
+ #
279
+ # users = swivel.call '/rest/users'
280
+ # users.class # => Swivel::List
281
+ # users.collect do |u| u.class end # => [Swivel::User, Swivel::User, ...]
282
+
283
+ class List < Response
284
+
285
+ # Instantiate a new Swivel::List. Calls super, then does a bit more extra processing.
286
+ def initialize *args
287
+ super *args
288
+ unless @processed
289
+ @list = Array.new
290
+ resource = @doc.elements[1].attributes['resource']
291
+ selector = "/list/#{resource}"
292
+ REXML::XPath.each @doc, selector do |e|
293
+ @list << Response.class_for(resource).new(e.to_s, @connection)
294
+ end
295
+ @processed = true
296
+ end
297
+ end
298
+
299
+ def refresh!
300
+ self # don't call refresh! on a list
301
+ end
302
+
303
+ # Delegates methods to the underlying Array instance. Allows you to
304
+ # call Array methods on a Swivel::List.
305
+ #
306
+ # data_sets = swivel.call '/rest/data_sets'
307
+ # # try some Array methods...
308
+ # data_sets.length.is_a? Integer # => true
309
+ # data_sets.first.is_a? Swivel::DataSet #=> true
310
+
311
+ def method_missing method_id, *args, &block
312
+ @list.send method_id, *args, &block
313
+ end
314
+ end
315
+
316
+ class Connection
317
+
318
+ # Encapsulates ~/.swivelrc configuration files. swivelrc files are just yaml text,
319
+ # so you're encouraged to manually edit.
320
+ #
321
+ # Load a ~/.swivelrc configuration, creating a default one if it doesn't exist.
322
+ # config = Swivel::Config.new
323
+ #
324
+ # Load configuration from a different file
325
+ # config.load 'different_configuration.yml'
326
+ #
327
+ # Save configuration to file
328
+ # config.save
329
+ #
330
+
331
+ class Config
332
+ CONFIG_DIR = ENV['HOME']
333
+ CONFIG_FILE = '.swivelrc'
334
+ DEFAULT_CONFIG = { :api_key => '', :protocol => 'http://',
335
+ :host => 'api.swivel.com', :port => 80,
336
+ :timeout_up => 200, :timeout_down => 100 }
337
+
338
+ attr_accessor :config
339
+
340
+ # Returns the hash that stores the configuration settings.
341
+ def config
342
+ @config ||= self.load
343
+ end
344
+
345
+ # Loads a configuration, which is then accessible through config.
346
+ def load filename = nil
347
+ filename ||= CONFIG_DIR + '/' + CONFIG_FILE
348
+ @filename = filename
349
+ dir = File.dirname @filename
350
+ FileUtils::mkdir_p dir unless File.exist? dir
351
+ @config =
352
+ unless File.exist? @filename
353
+ DEFAULT_CONFIG
354
+ else
355
+ YAML::load_file @filename
356
+ end
357
+ end
358
+
359
+ # Saves a configuration to the same file from which it was loaded.
360
+ def save
361
+ open @filename, 'w+' do |f|
362
+ YAML::dump @config, f
363
+ end
364
+ end
365
+ end
366
+
367
+ attr_accessor :config
368
+
369
+ # Instantiate a connection to Swivel. Use the connection to query or upload,
370
+ # append, or replace data. Passed in options will take precedence over values
371
+ # set in ~/.swivelrc.
372
+
373
+ def initialize options = Hash.new
374
+ @config = Config.new.config
375
+ @config.keys.each do |key|
376
+ @config[key] = options[key] if options.has_key? key
377
+ end
378
+ @headers = options[:headers] || Hash.new
379
+ @headers.merge! 'Accept' => 'application/xml'
380
+ if @config.has_key?(:api_key) && !@config[:api_key].blank?
381
+ encoded = Base64.encode64(':' + @config[:api_key])
382
+ @headers.merge! 'Authorization' => "Basic #{encoded}"
383
+ end
384
+ end
385
+
386
+ # Call Swivel's REST endpoint. This method actually performs the HTTP stuff
387
+ # that you need. and returns objects constructed from the returned XML. If an
388
+ # appropriate class is not available, just returns the XML string.
389
+ #
390
+ # # returns an object that's a subclass of Swivel::Response
391
+ # user = swivel.call '/rest/users/1000010'
392
+ # user.class # => Swivel::User
393
+ #
394
+ # users = swivel.call '/rest/users'
395
+ # users.class # => Swivel::List
396
+ #
397
+ # # returns a string (because an appropriate Swivel::Response subclass doesn't exist)
398
+ # echo = swivel.call '/rest/test/echo/howdy'
399
+ # echo.class # => String
400
+
401
+ def call path, params = Hash.new, method = :get
402
+ xml =
403
+ Net::HTTP.start @config[:host], @config[:port] do |http|
404
+ request = "Net::HTTP::#{method.to_s.camelize}".constantize.new path, @headers
405
+ if [:delete, :post, :put].include? method
406
+ http.read_timeout = @config[:timeout_up]
407
+ http.request request, params.to_query_string
408
+ else
409
+ http.read_timeout = @config[:timeout_down]
410
+ http.request request
411
+ end
412
+ end.body
413
+ doc = REXML::Document.new xml
414
+ Response.class_for(doc.root.elements[1].name).new xml, self
415
+ rescue Exception => e
416
+ xml || nil
417
+ end
418
+
419
+ # Performs an upload, append, or replace. Set options[:mode] to one of "initial",
420
+ # "append", or "replace". If unset, options[:mode] defaults to "initial". Append
421
+ # and replace can also be called directly, without setting options[:mode].
422
+ #
423
+ # In order to append or replace a data_set, you must be the owner of the data.
424
+ # The new data must conform to the same column structure as the original data.
425
+ #
426
+ # In order to upload (including replace and append), you must have a valid api key.
427
+ #
428
+ # TODO: outline required and optional parameters. give a few examples.
429
+ #
430
+ # TODO: elaborate on requirements/assumptions for replace and append modes.
431
+ #
432
+ # TODO: limitations and crap?
433
+ #
434
+ # # upload a file to swivel
435
+ # data_set = swivel.upload {...}
436
+ #
437
+ # # append to a data_set already in Swivel
438
+ # data_set = swivel.append {...}
439
+ #
440
+ # # replace underlying data in a data_set that already exists on Swivel
441
+ # data_set = swivel.replace {...}
442
+
443
+ def upload options = Hash.new
444
+ options[:mode] ||= 'initial'
445
+ uri, method =
446
+ case options[:mode]
447
+ when 'append', 'replace'
448
+ ["/rest/data_sets/#{options[:id]}", :put]
449
+ else
450
+ ['/rest/data_sets', :post]
451
+ end
452
+ call uri, options, method
453
+ end
454
+
455
+ %w/append replace/.each do |mode|
456
+ class_eval <<-LAZY
457
+ def #{mode} options = Hash.new
458
+ options[:mode] = "#{mode}"
459
+ upload options
460
+ end
461
+ LAZY
462
+ end
463
+ end
464
+ end
metadata ADDED
@@ -0,0 +1,58 @@
1
+ --- !ruby/object:Gem::Specification
2
+ rubygems_version: 0.9.2
3
+ specification_version: 1
4
+ name: swivel
5
+ version: !ruby/object:Gem::Version
6
+ version: 0.0.1
7
+ date: 2007-06-15 00:00:00 -07:00
8
+ summary: Ruby interface to the Swivel API.
9
+ require_paths:
10
+ - lib
11
+ email: huned@swivel.com
12
+ homepage: http://swivel.com/developer
13
+ rubyforge_project:
14
+ description: This gem installs client library for accessing Swivel through it's API.
15
+ autorequire:
16
+ default_executable:
17
+ bindir: bin
18
+ has_rdoc: true
19
+ required_ruby_version: !ruby/object:Gem::Version::Requirement
20
+ requirements:
21
+ - - ">"
22
+ - !ruby/object:Gem::Version
23
+ version: 0.0.0
24
+ version:
25
+ platform: ruby
26
+ signing_key:
27
+ cert_chain:
28
+ post_install_message:
29
+ authors:
30
+ - huned
31
+ files:
32
+ - COPYING
33
+ - README
34
+ - Rakefile
35
+ - lib/swivel.rb
36
+ - bin/swivel
37
+ - CHANGELOG
38
+ test_files: []
39
+
40
+ rdoc_options:
41
+ - --line-numbers
42
+ - --title
43
+ - swivel.rb
44
+ - --main
45
+ - README
46
+ - --inline-source
47
+ extra_rdoc_files:
48
+ - README
49
+ - CHANGELOG
50
+ - COPYING
51
+ executables: []
52
+
53
+ extensions: []
54
+
55
+ requirements: []
56
+
57
+ dependencies: []
58
+