swivel 0.0.1

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.
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
+