swivel 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +4 -0
- data/COPYING +18 -0
- data/README +315 -0
- data/Rakefile +80 -0
- data/bin/swivel +135 -0
- data/lib/swivel.rb +464 -0
- metadata +58 -0
data/CHANGELOG
ADDED
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
|
+
|