neoneo 0.1.0

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/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