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