neoneo 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/History.txt ADDED
@@ -0,0 +1,6 @@
1
+ === 0.1.0 / 2008-10-23
2
+
3
+ * Initial Release
4
+
5
+ * First release.
6
+
data/Manifest.txt ADDED
@@ -0,0 +1,7 @@
1
+ History.txt
2
+ Manifest.txt
3
+ README.txt
4
+ Rakefile
5
+ lib/neoneo.rb
6
+ spec/spec_helper.rb
7
+ spec/neoneo_spec.rb
data/README.txt ADDED
@@ -0,0 +1,88 @@
1
+ = NeoNeo
2
+
3
+ == DESCRIPTION:
4
+
5
+ NeoNeo is a Ruby wrapper to access No Kahuna (www.nokahuna.com) from within your Ruby projects.
6
+
7
+ It uses mechanize and tends to be a bit slow but hey, it's the only ready to use
8
+ library today so enjoy it, improve it or go away ;)
9
+
10
+ == SYNOPSIS:
11
+
12
+ require 'rubygems'
13
+ require 'neoneo'
14
+
15
+ project = Neoneo::User.new('user name', 'password').projects.find('My Project')
16
+ project.name = "Cool project"
17
+ project.save
18
+
19
+ p project.tasks.map {|t| t.name}
20
+
21
+ project.categories.find('An existing Category').add_task("Cool new Task",
22
+ :assign_to => 'Some Username')
23
+
24
+ project.add_task("Another way to add a task and notify ANYONE ;)",
25
+ :notify => project.members, :category => "Some Category")
26
+
27
+ == REQUIREMENTS:
28
+
29
+ * Mechanize (http://mechanize.rubyforge.org/mechanize/)
30
+
31
+ == INSTALL:
32
+
33
+ * sudo gem install neoneo
34
+
35
+ == PERFORMANCE:
36
+
37
+ As No Kahuna does not provide a real API to their services Neoneo wraps the
38
+ normal HTML pages as you can see them in your browser. This means not thaaaat
39
+ speedy performance. Especially because the No Kahuna guys using Rails cool
40
+ CSRF avoiding technology and deliver any form with a token which you have to
41
+ sent back to the server to confirm that you're not working on a stolen session.
42
+ Due to this e.g. the login procedure consists of THREE HTTP request :/
43
+ 1. Set the language of the interface to english to allow Neoneo to parse any
44
+ messages correctly
45
+ 2. Get the login form (with that token)
46
+ 3. Post that login form
47
+
48
+ But I've tried hard to suck as much information as possible out of any HTML
49
+ page Neoneo is receiving and to lazy-load most of the details.
50
+ E.g. when you log in Neoneo can scan all your projects etc from the initial page
51
+ you get after a login. So no second (or forth to be correct ;) ) request is
52
+ needed to get a project list. And if you would like to get all the members of
53
+ a specific project Neoneo checks if they are already present, if not the
54
+ projects detail page is loaded and all the members are gatherd (along with any
55
+ other useful information from that page). If you call the members method again,
56
+ no new request is needed.
57
+
58
+ Just want to let you know all this. Neoneo is usable but it's more like a
59
+ No Kahuna Information Delivery Bus than a No Kahuna Dragster!
60
+
61
+ == Etymology
62
+ Neoneo as the hawaiian word for chaos is just what is logical caused by
63
+ No Kahuna ;)
64
+
65
+ == LICENSE:
66
+
67
+ (The MIT License)
68
+
69
+ Copyright (c) 2008 FIX
70
+
71
+ Permission is hereby granted, free of charge, to any person obtaining
72
+ a copy of this software and associated documentation files (the
73
+ 'Software'), to deal in the Software without restriction, including
74
+ without limitation the rights to use, copy, modify, merge, publish,
75
+ distribute, sublicense, and/or sell copies of the Software, and to
76
+ permit persons to whom the Software is furnished to do so, subject to
77
+ the following conditions:
78
+
79
+ The above copyright notice and this permission notice shall be
80
+ included in all copies or substantial portions of the Software.
81
+
82
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
83
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
84
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
85
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
86
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
87
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
88
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,19 @@
1
+ # -*- ruby -*-
2
+
3
+ require 'rubygems'
4
+ require 'hoe'
5
+ require 'mechanize'
6
+ require './lib/neoneo.rb'
7
+
8
+ Hoe.new('neoneo', Neoneo::VERSION) do |p|
9
+ p.rubyforge_name = "kickassrb"
10
+ p.name = "neoneo"
11
+ p.author = "Thorben Schröder"
12
+ p.description = "Ruby wrapper to access No Kahuna (www.nokahuna.com) from within your Ruby projects."
13
+ p.email = 'thorben@fetmab.net'
14
+ p.summary = "Ruby wrapper to access No Kahuna (www.nokahuna.com) from within your Ruby projects."
15
+ p.changes = p.paragraphs_of('History.txt', 0..1).join("\n\n")
16
+ p.extra_deps << ['mechanize'," >=0.8.4"]
17
+ end
18
+
19
+ # vim: syntax=Ruby
data/lib/neoneo.rb ADDED
@@ -0,0 +1,472 @@
1
+ $LOAD_PATH << File.dirname(__FILE__)
2
+
3
+ module Neoneo
4
+ require 'rubygems'
5
+ require 'mechanize'
6
+
7
+ require 'hpricot_extensions'
8
+ require 'utils'
9
+
10
+
11
+ BASE_URL = 'http://nokahuna.com/'
12
+ PROJECT_URL = "#{BASE_URL}projects/"
13
+ VERSION = '0.1.0'
14
+
15
+ # A Neoneo::AuthenticationError is thrown whenever No Kahuna reports, that
16
+ # you're not logged in properly.
17
+ class AuthenticationError < StandardError; end
18
+
19
+ # The default/fallback Error in NeoNeo
20
+ #
21
+ # Neoneo::Error is the default error that is thown any time No Kahuna reports
22
+ # and error which Neoneo is not able to handle properly. Maybe because there
23
+ # is no error handler for that kind of error implemented or because No Kahuna
24
+ # changed it's interface and Neoneo has not yet been updated to reflect those
25
+ # changes
26
+ class Error < ArgumentError; end
27
+
28
+ # Normal array with a few select extensions
29
+ class SingleSelectArray < Array
30
+
31
+ # Find an item in the array by it's name
32
+ def find(name)
33
+ self.select {|item| item.name == name}.first
34
+ end
35
+
36
+ # Find an item in the array by it's name when value is a string.
37
+ # If the passed value is a Member or Category object just return that and
38
+ # if none of those rules apply return nil.
39
+ #
40
+ # This method allows the user e.g. to assign new task by just using the
41
+ # name of the project member and not it's corresponding Member object.
42
+ def find_or_use(value)
43
+ case value
44
+ when String
45
+ result = find(value)
46
+ when Member, Category, Project
47
+ result = value
48
+ else
49
+ result = nil
50
+ end
51
+ result
52
+ end
53
+ end
54
+
55
+ # Wrapper around the WWW::Mechanize object to allow an easy and DRY error
56
+ # handling.
57
+ #
58
+ # Ath the moment only the get, post and submit methods are subject of this
59
+ # error handling.
60
+ class Agent < WWW::Mechanize
61
+ def get(url)
62
+ page = super(url)
63
+ handle_errors(page)
64
+ end
65
+
66
+ def post(url, options = {})
67
+ page = super(url, options)
68
+ handle_errors(page)
69
+ end
70
+
71
+ def submit(form)
72
+ page = super(form)
73
+ handle_errors(page)
74
+ end
75
+
76
+ # This methos actually does the error handling.
77
+ #
78
+ # When an error message is found in the response from No Kahuna it's
79
+ # text determines which error is thrown.
80
+ # If the error message does not match any of the specific messages Neoneo
81
+ # tries to catch, a default Neoneo::Error is thrown.
82
+ def handle_errors(page)
83
+ return page if page.instance_of? WWW::Mechanize::File
84
+
85
+ errors = page.search('div#flash.error p').map {|e| e.innerText}
86
+ errors.each do |error|
87
+ case error
88
+ when 'Invalid login or password.'
89
+ raise AuthenticationError
90
+ else
91
+ raise Error.new(e)
92
+ end
93
+ end
94
+
95
+ page
96
+ end
97
+ end
98
+
99
+ # The starting point for any use of the NeoNeo library.
100
+ #
101
+ # Other than a Neoneo::Member iy represents a user of No Kahuna of wich you
102
+ # have the full login credentials.
103
+ #
104
+ # To initialize a connection to No Kahuna start with:
105
+ #
106
+ # Neoneo::User.new('User Name', 'Password')
107
+ #
108
+ # Neoneo then loggs you in to No Kahuna and gathers some first informations
109
+ # about your projects, task counts and so on.
110
+ #
111
+ # Unfortunately the initialization process needs to do actually three HTTP
112
+ # requests at the moment. First it sets your language to English, than it
113
+ # has to get the login form to be aware of the CSRF id to actually log you
114
+ # in in a third request, the submission of the login form.
115
+ #
116
+ # Also there is no way to check the stay logged in option of No Kahuna yet.
117
+ # This is planned for a future version.
118
+ class User
119
+ attr_reader :projects, :authenticity_token, :agent
120
+
121
+ def initialize(user, pass)
122
+ @agent = Agent.new
123
+
124
+ @agent.post("#{BASE_URL}settings/use_locale?locale=en-US")
125
+
126
+ page = @agent.get("#{BASE_URL}login")
127
+
128
+ form = page.forms.first
129
+ @authenticity_token = form.authenticity_token
130
+ form.login = user
131
+ form.password = pass
132
+
133
+ page = @agent.submit(form)
134
+
135
+ @projects = SingleSelectArray.new
136
+
137
+ page.search('ul.projectList li a').each do |project_link|
138
+ name = project_link.children.last.clean
139
+ total_taks = project_link.search('span.taskCount span.total').first.clean
140
+ own_tasks = project_link.search('span.taskCount').first.children.first.clean.gsub(/^(\d+)\s\//, '\1').to_i
141
+ id = project_link.attributes['href'].gsub(/^#{PROJECT_URL}(\d+)\/.*$/, '\1')
142
+
143
+ @projects << Project.new(id, name, total_taks, own_tasks, self)
144
+ end
145
+
146
+ end
147
+
148
+ end
149
+
150
+ # Representation of No Kahuna's projects.
151
+ #
152
+ # It holds all information about a project, like it's name and description,
153
+ # it's categories, members and tasks. It's also used to add new tasks to
154
+ # a project and can also be used to change the name and description of the
155
+ # project.
156
+ class Project
157
+ attr_reader :id, :agent, :user
158
+ attr_accessor :name, :total_tasks, :own_tasks, :description
159
+
160
+ def initialize(id, name, total_tasks, own_tasks, user)
161
+ @id = id
162
+ @name = name
163
+ @total_tasks_count = total_tasks
164
+ @own_tasks_count = own_tasks
165
+
166
+ @user = user
167
+ end
168
+
169
+ def description
170
+ unless @description
171
+ page = user.agent.get(url)
172
+ @description = page.search('div.projectDescription p').last.clean
173
+ end
174
+ @description
175
+ end
176
+
177
+ def description=(new_description)
178
+ @description = new_description
179
+ end
180
+
181
+ def categories
182
+ build_categories!(user.agent.get(url('tasks/new'))) unless @categories
183
+
184
+ @categories
185
+ end
186
+
187
+ def members
188
+ build_members!(user.agent.get(url('tasks/new'))) unless @members
189
+
190
+ @members
191
+ end
192
+
193
+ # The open tasks of the project
194
+ def tasks
195
+ build_tasks!(user.agent.get(url('tasks?group_by=category'))) unless @tasks
196
+
197
+ @tasks
198
+ end
199
+
200
+ # The closed tasks of the project
201
+ #
202
+ # For technical reasons I decided to devide the tasks into closed and open
203
+ # ones hoping that nobody really needs the closed ones ;)
204
+ # The problem is: At the moment the only chance to get an overview of closed
205
+ # tasks in the No Kahuna interface is to search for 'task'. But then
206
+ # you get no category information with the tasks. So I decided that it is
207
+ # more important to have the category information for any task available
208
+ # without the need to load a single page for any task which is possible
209
+ # with the normal task overview and which the tasks method does.
210
+ # If you really would like to see the closed tasks use this method by be
211
+ # aware, that if you access the category of a closed task a new HTTP request
212
+ # has to made!
213
+ #
214
+ # Also watch out for an other pitfall: If you close or reopen a task they
215
+ # stay in their original array! So if you do
216
+ # project.tasks.first.close!
217
+ # a call to
218
+ # project.tasks
219
+ # just after that would INCLUDE the closed task and if you already had
220
+ # called closed_tasks another call to that would NOT INCLUDE the closed
221
+ # task!
222
+ def closed_tasks
223
+ build_closed_tasks!(user.agent.get(url('tasks/search?s=task'))) unless @closed_tasks
224
+
225
+ @closed_tasks
226
+ end
227
+
228
+ # Adds a task to a project.
229
+ # The options hash can consist of the following keys:
230
+ # - :category => 'Some Category Name' OR some_category_object
231
+ # - :assign_to => 'Some User Name' OR some_member_object
232
+ # - :notify => 'Some User Name' OR some_member_object OR an array of them
233
+ # An example:
234
+ # project = Neoneo::User.new('John Doe', 'god').projects.find('My Project')
235
+ # project.add_task("A shiny new task",
236
+ # :assign_to => 'Bob Dillan',
237
+ # :category => project.categories.first,
238
+ # :notify => ['John Doe', project.members.last])
239
+ def add_task(description, options = {})
240
+ page = user.agent.get(url('tasks/new'))
241
+
242
+ build_categories!(page) unless @categories
243
+ build_members!(page) unless @members
244
+
245
+ category = categories.find_or_use(options[:category])
246
+ assign_to = members.find_or_use(options[:assign_to])
247
+
248
+ notifications = Array.new
249
+ case options[:notify]
250
+ when Array
251
+ options[:notify].each do |member|
252
+ notifications << members.find_or_use(member)
253
+ end
254
+ else
255
+ notifications << members.find_or_use(options[:notify])
256
+ end
257
+ notifications.compact!
258
+
259
+ page = user.agent.get(url('tasks/new'))
260
+ form = page.forms.last
261
+
262
+ form.send('task[body]'.to_sym, description)
263
+ form.send('task[assigned_to_id]'.to_sym, assign_to.id) if assign_to
264
+ form.send('task[category_id]'.to_sym, category.id) if category
265
+
266
+ notifications.each do |notification|
267
+ form.add_field!('subscriber_ids[]', notification.id)
268
+ end
269
+
270
+ user.agent.submit form
271
+ end
272
+
273
+ # Saves the project name and descriptions which you can set simply with
274
+ # name= and description=
275
+ # An example:
276
+ # project = Neoneo::User.new('John Doe', 'god').projects.find('My Project')
277
+ # project.name = 'BLA!'
278
+ # project.description = 'New description'
279
+ # project.save
280
+ def save
281
+ page = user.agent.get(url('edit'))
282
+ form = page.forms.last
283
+ form.send('project[name]='.to_sym, @name)
284
+ form.send('project[description]='.to_sym, @description) if @description
285
+ page = user.agent.submit form
286
+
287
+ raise Error unless page.search('div#flash.notice p').first.clean ==
288
+ 'Successfully saved project'
289
+ end
290
+
291
+ # The URL to the project at No Kahuna
292
+ def url(appendix = '')
293
+ "#{PROJECT_URL}#{@id}/#{appendix}"
294
+ end
295
+
296
+ private
297
+
298
+ def build_members!(page)
299
+ @members = SingleSelectArray.new
300
+
301
+ members = page.search('select#task_assigned_to_id option')
302
+ members.each do |member|
303
+ id = member.attributes['value']
304
+ @members << Member.new(id, member.innerText, self) unless id.empty?
305
+ end
306
+ end
307
+
308
+ def build_categories!(page)
309
+ @categories = SingleSelectArray.new
310
+
311
+ categories = page.search('select#task_category_id option')
312
+ categories.each do |category|
313
+ id = category.attributes['value']
314
+ @categories << Category.new(id, category.innerText, self) unless id.empty?
315
+ end
316
+ end
317
+
318
+
319
+ def build_tasks!(page)
320
+ @tasks = SingleSelectArray.new
321
+
322
+ categories = page.search('div#task_list_grouped_by_category div.taskList')
323
+
324
+ categories.each do |category_div|
325
+ category = self.categories.find(category_div.search('h2').first.clean)
326
+ tasks = category_div.search('ul.sortable_tasks li')
327
+ tasks.each do |task_item|
328
+ user = Utils::URL.url_unescape(task_item.search('span.avatar a').first.attributes['href'].gsub(/^\/users\//, ''))
329
+ task_link = task_item.search('a.taskLink')
330
+ id = task_link.search('span.taskId').first.clean
331
+ description = task_link.search('span.taskShortBody').first.clean
332
+ @tasks << Task.new(id, description, category, members.find(user), self)
333
+ end
334
+ end
335
+ end
336
+
337
+ def build_closed_tasks!(page)
338
+ @closed_tasks = SingleSelectArray.new
339
+
340
+ tasks = page.search('div#tasks_for_me ul.search li.done')
341
+
342
+ tasks.each do |task_item|
343
+ user = Utils::URL.url_unescape(task_item.search('span.avatar a').first.attributes['href'].gsub(/^\/users\//, ''))
344
+ task_link = task_item.search('a.taskLink')
345
+ id = task_link.search('span.taskId').first.clean
346
+ description = task_link.search('span.taskShortBody').first.clean
347
+ @closed_tasks << Task.new(id, description, nil, members.find(user), self, true)
348
+ end
349
+ end
350
+ end
351
+
352
+ # Representation of No Kahuna's categories for tasks
353
+ #
354
+ # At the moment it's a read-only class. So you can't change the name of a
355
+ # category and then save those change.
356
+ # This is planned for future versions.
357
+ class Category
358
+ attr_reader :id, :name
359
+
360
+ def initialize(id, name, project)
361
+ @id = id
362
+ @name = name
363
+ @project = project
364
+ end
365
+
366
+ # Adds a task to this category
367
+ #
368
+ # It works exactly as Project#add_task only with a pre-filled :category
369
+ # option. So please look there for further instructions on how to use it.
370
+ def add_task(description, options = {})
371
+ options[:category => self]
372
+ @project.add_task(description, options)
373
+ end
374
+ end
375
+
376
+
377
+ # Representation of No Kahuna users who are members of a project.
378
+ #
379
+ # This class is different to the User class! While a Neoneo::User describes
380
+ # a No Kahuna user from which you know the login credentials to No Kahuna
381
+ # a Neoneo::Member represents a No Kahuna user only with all the information
382
+ # you can get about it by sharing a project with your Neoneo::User.
383
+ # As you can imagine this is again a read-only class so no changes can be made
384
+ # (how should they, you don't know the member's password by definition!)
385
+ # And yes, even your Neoneo::User will be represented as a Neoneo::Member
386
+ # When you call Project#members. This is to avoid any confusion by dealing
387
+ # with two different classes with different abilities in one and the same
388
+ # array.
389
+ class Member
390
+ attr_reader :id, :name
391
+
392
+ def initialize(id, name, project)
393
+ @id = id
394
+ @name = name
395
+ @project = project
396
+ end
397
+ end
398
+
399
+ # Represents a No Kahuna task
400
+ #
401
+ # At the moment this class is read-only in regards of it's description.
402
+ # But you can close or reopen a task.
403
+ # This will be improved as soon as possible!
404
+ class Task
405
+ attr_reader :id, :user, :project
406
+
407
+ def initialize(id, description, category, user, project, done = false)
408
+ @id = id
409
+ @description = description
410
+ @category = category
411
+ @user = user
412
+ @project = project
413
+ @done = done
414
+
415
+ @uncertain = @description =~ /\.{3}$/
416
+ end
417
+
418
+ def description
419
+ build_description! if @uncertain
420
+ @description
421
+ end
422
+
423
+ def category
424
+ build_category! unless @category
425
+
426
+ @category
427
+ end
428
+
429
+ def url(appendix = '')
430
+ @project.url("tasks/#{@id}/#{appendix}")
431
+ end
432
+
433
+ def close!
434
+ page = @project.user.agent.get(url)
435
+ form = page.forms.last
436
+ authenticity_token = form.authenticity_token
437
+
438
+ @project.user.agent.post(url('done'), :authenticity_token => authenticity_token, '_method'.to_sym => 'put')
439
+ @done = true
440
+ end
441
+
442
+ def reopen!
443
+ return unless @done
444
+
445
+ page = @project.user.agent.get(url)
446
+ form = page.forms.last
447
+ authenticity_token = form.authenticity_token
448
+
449
+ @project.user.agent.post(url('not_done'), :authenticity_token => authenticity_token, '_method'.to_sym => 'put')
450
+ @done = false
451
+ end
452
+
453
+ def closed?
454
+ @done
455
+ end
456
+
457
+ private
458
+ def build_description!(page = nil)
459
+ page ||= project.user.agent.get(url('edit'))
460
+ form = page.forms.last
461
+ @description = form.send('task[body]'.to_sym)
462
+ @uncertain = false
463
+ end
464
+
465
+ def build_category!(page = nil)
466
+ page ||= project.user.agent.get(url('edit'))
467
+
468
+ @category = project.categories.find(page.search('span.category').clear)
469
+ end
470
+ end
471
+
472
+ end
@@ -0,0 +1,4 @@
1
+ require File.join(File.dirname(__FILE__), 'spec_helper')
2
+
3
+ describe Neoneo do
4
+ end
@@ -0,0 +1,9 @@
1
+ begin
2
+ require 'spec'
3
+ rescue LoadError
4
+ require 'rubygems'
5
+ gem 'rspec'
6
+ require 'spec'
7
+ end
8
+
9
+ require File.join(File.dirname(__FILE__), '..', 'lib', 'neoneo')
data.tar.gz.sig ADDED
Binary file
metadata ADDED
@@ -0,0 +1,102 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: neoneo
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - "Thorben Schr\xC3\xB6der"
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain:
11
+ - |
12
+ -----BEGIN CERTIFICATE-----
13
+ MIIDMjCCAhqgAwIBAgIBADANBgkqhkiG9w0BAQUFADA/MRAwDgYDVQQDDAd0aG9y
14
+ YmVuMRYwFAYKCZImiZPyLGQBGRYGZmV0bWFiMRMwEQYKCZImiZPyLGQBGRYDbmV0
15
+ MB4XDTA4MTAwOTIxNTQwNloXDTA5MTAwOTIxNTQwNlowPzEQMA4GA1UEAwwHdGhv
16
+ cmJlbjEWMBQGCgmSJomT8ixkARkWBmZldG1hYjETMBEGCgmSJomT8ixkARkWA25l
17
+ dDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAOTrBWJAhXdd4FALdaz4
18
+ +2KKe6Loz7L8AQxvhYedX7trYpqrWmXNLyCZKvNDf7Hp0EmOn8k5Iti161bcWxwY
19
+ fj8ejQ02U3OUyKSQM7V7zUrzB9pkmZ8ROGWJmw+nWVu7ZF7UU6+kWwaSMU/unPno
20
+ c1PcfOQrwCjvXbedMTFPZ1b/W37DoaoVQJLzzx95ewXSZ7iPtLxTrjHESjWBPxFi
21
+ JMEVCZDM+5UTEm41ucAJJ58z54mKryRap4NMux9YmPFp13f0xFVKP5kST16Q96IV
22
+ qJaPKd4WApsB8WOOyxGVFzp6Lf1fAHKjrXca6ywHeAM070Ki6GzAXKPBzUV13/R7
23
+ azECAwEAAaM5MDcwCQYDVR0TBAIwADALBgNVHQ8EBAMCBLAwHQYDVR0OBBYEFMCo
24
+ Q0fsO/qD4FD6zVoAIBw5ehlOMA0GCSqGSIb3DQEBBQUAA4IBAQCT+qucnHSHu9t0
25
+ Ntxpnm5gpQPVFz+kI6WCAqUeVlV5cbifH7/T+HKEePe+H3sF1eHG2X0QiXMYDZ26
26
+ Vgp6S9LCofXhJySOGYO26gcUyGfmkmQ//+YiwpJ0k+uznEM+RBNw/CSpFoXrnKa2
27
+ 39/buzR3VtgPAcAOHb+5+WDIdX6NGgrKFF8udOqQ+rAvsoQXpJXfpfdqoFiOdfCa
28
+ Bqd6tQGVy0qUttoqMCOTxwMYWzoNs5GFXqtmbXxV6W2F81ipkELVVoWtSvRRkqtx
29
+ dX2CCcpgG+qXnji1CJyb6Dgm5ICJO/+B8ZKQ5qAYg798KOB7gyddzwRZWImtRoYU
30
+ kX4sVHCM
31
+ -----END CERTIFICATE-----
32
+
33
+ date: 2008-10-23 00:00:00 +02:00
34
+ default_executable:
35
+ dependencies:
36
+ - !ruby/object:Gem::Dependency
37
+ name: mechanize
38
+ type: :runtime
39
+ version_requirement:
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ requirements:
42
+ - - ">="
43
+ - !ruby/object:Gem::Version
44
+ version: 0.8.4
45
+ version:
46
+ - !ruby/object:Gem::Dependency
47
+ name: hoe
48
+ type: :development
49
+ version_requirement:
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: 1.8.0
55
+ version:
56
+ description: Ruby wrapper to access No Kahuna (www.nokahuna.com) from within your Ruby projects.
57
+ email: thorben@fetmab.net
58
+ executables: []
59
+
60
+ extensions: []
61
+
62
+ extra_rdoc_files:
63
+ - History.txt
64
+ - Manifest.txt
65
+ - README.txt
66
+ files:
67
+ - History.txt
68
+ - Manifest.txt
69
+ - README.txt
70
+ - Rakefile
71
+ - lib/neoneo.rb
72
+ - spec/spec_helper.rb
73
+ - spec/neoneo_spec.rb
74
+ has_rdoc: true
75
+ homepage:
76
+ post_install_message:
77
+ rdoc_options:
78
+ - --main
79
+ - README.txt
80
+ require_paths:
81
+ - lib
82
+ required_ruby_version: !ruby/object:Gem::Requirement
83
+ requirements:
84
+ - - ">="
85
+ - !ruby/object:Gem::Version
86
+ version: "0"
87
+ version:
88
+ required_rubygems_version: !ruby/object:Gem::Requirement
89
+ requirements:
90
+ - - ">="
91
+ - !ruby/object:Gem::Version
92
+ version: "0"
93
+ version:
94
+ requirements: []
95
+
96
+ rubyforge_project: kickassrb
97
+ rubygems_version: 1.2.0
98
+ signing_key:
99
+ specification_version: 2
100
+ summary: Ruby wrapper to access No Kahuna (www.nokahuna.com) from within your Ruby projects.
101
+ test_files: []
102
+
metadata.gz.sig ADDED
Binary file