basecamp 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (3) hide show
  1. data/lib/basecamp.rb +650 -0
  2. data/readme +13 -0
  3. metadata +146 -0
@@ -0,0 +1,650 @@
1
+ require 'net/https'
2
+ require 'yaml'
3
+ require 'date'
4
+ require 'time'
5
+
6
+ begin
7
+ require 'xmlsimple'
8
+ rescue LoadError
9
+ begin
10
+ require 'rubygems'
11
+ require 'xmlsimple'
12
+ rescue LoadError
13
+ abort <<-ERROR
14
+ The 'xml-simple' library could not be loaded. If you have RubyGems installed
15
+ you can install xml-simple by doing "gem install xml-simple".
16
+ ERROR
17
+ end
18
+ end
19
+
20
+ begin
21
+ require 'activeresource'
22
+ rescue LoadError
23
+ begin
24
+ require 'rubygems'
25
+ require 'activeresource'
26
+ rescue LoadError
27
+ abort <<-ERROR
28
+ The 'activeresource' library could not be loaded. If you have RubyGems
29
+ installed you can install ActiveResource by doing "gem install activeresource".
30
+ ERROR
31
+ end
32
+ end
33
+
34
+ # = A Ruby library for working with the Basecamp web-services API.
35
+ #
36
+ # For more information about the Basecamp web-services API, visit:
37
+ #
38
+ # http://developer.37signals.com/basecamp
39
+ #
40
+ # NOTE: not all of Basecamp's web-services are accessible via REST. This
41
+ # library provides access to RESTful services via ActiveResource. Services not
42
+ # yet upgraded to REST are accessed via the Basecamp class. Continue reading
43
+ # for more details.
44
+ #
45
+ #
46
+ # == Establishing a Connection
47
+ #
48
+ # The first thing you need to do is establish a connection to Basecamp. This
49
+ # requires your Basecamp site address and your login credentials. Example:
50
+ #
51
+ # Basecamp.establish_connection!('you.grouphub.com', 'username', 'password')
52
+ #
53
+ # This is the same whether you're accessing using the ActiveResource interface,
54
+ # or the legacy interface.
55
+ #
56
+ #
57
+ # == Using the REST interface via ActiveResource
58
+ #
59
+ # The REST interface is accessed via ActiveResource, a popular Ruby library
60
+ # that implements object-relational mapping for REST web-services. For more
61
+ # information on working with ActiveResource, see:
62
+ #
63
+ # * http://api.rubyonrails.org/files/activeresource/README.html
64
+ # * http://api.rubyonrails.org/classes/ActiveResource/Base.html
65
+ #
66
+ #
67
+ # === Finding a Resource
68
+ #
69
+ # Find a specific resource using the +find+ method. Attributes of the resource
70
+ # are available as instance methods on the resulting object. For example, to
71
+ # find a message with the ID of 8675309 and access its title attribute, you
72
+ # would do the following:
73
+ #
74
+ # m = Basecamp::Message.find(8675309)
75
+ # m.title # => 'Jenny'
76
+ #
77
+ # To find all messages for a given project, use find(:all), passing the
78
+ # project_id as a parameter to find. Example:
79
+ #
80
+ # messages = Basecamp::Message.find(:all, params => { :project_id => 1037 })
81
+ # messages.size # => 25
82
+ #
83
+ #
84
+ # === Creating a Resource
85
+ #
86
+ # Create a resource by making a new instance of that resource, setting its
87
+ # attributes, and saving it. If the resource requires a prefix to identify
88
+ # it (as is the case with resources that belong to a sub-resource, such as a
89
+ # project), it should be specified when instantiating the object. Examples:
90
+ #
91
+ # m = Basecamp::Message.new(:project_id => 1037)
92
+ # m.category_id = 7301
93
+ # m.title = 'Message in a bottle'
94
+ # m.body = 'Another lonely day, with no one here but me'
95
+ # m.save # => true
96
+ #
97
+ # c = Basecamp::Comment.new(:post_id => 25874)
98
+ # c.body = 'Did you get those TPS reports?'
99
+ # c.save # => true
100
+ #
101
+ # You can also create a resource using the +create+ method, which will create
102
+ # and save it in one step. Example:
103
+ #
104
+ # Basecamp::TodoItem.create(:todo_list_id => 3422, :contents => 'Do it')
105
+ #
106
+ #
107
+ # === Updating a Resource
108
+ #
109
+ # To update a resource, first find it by its id, change its attributes, and
110
+ # save it. Example:
111
+ #
112
+ # m = Basecamp::Message.find(8675309)
113
+ # m.body = 'Changed'
114
+ # m.save # => true
115
+ #
116
+ #
117
+ # === Deleting a Resource
118
+ #
119
+ # To delete a resource, use the +delete+ method with the ID of the resource
120
+ # you want to delete. Example:
121
+ #
122
+ # Basecamp::Message.delete(1037)
123
+ #
124
+ #
125
+ # === Attaching Files to a Resource
126
+ #
127
+ # If the resource accepts file attachments, the +attachments+ parameter should
128
+ # be an array of Basecamp::Attachment objects. Example:
129
+ #
130
+ # a1 = Basecamp::Attachment.create('primary', File.read('primary.doc'))
131
+ # a2 = Basecamp::Attachment.create('another', File.read('another.doc'))
132
+ #
133
+ # m = Basecamp::Message.new(:project_id => 1037)
134
+ # ...
135
+ # m.attachments = [a1, a2]
136
+ # m.save # => true
137
+ #
138
+ #
139
+ # = Using the non-REST inteface
140
+ #
141
+ # The non-REST interface is accessed via instance methods on the Basecamp
142
+ # class. Ensure you've established a connection, then create a new Basecamp
143
+ # instance and call methods on it. Object attributes are accessible as methods.
144
+ # Example:
145
+ #
146
+ # session = Basecamp.new
147
+ # person = session.person(93832) # => #<Record(person)..>
148
+ # person.first_name # => "Jason"
149
+ #
150
+ class Basecamp
151
+ class Connection #:nodoc:
152
+ def initialize(master)
153
+ @master = master
154
+ @connection = Net::HTTP.new(master.site, master.use_ssl ? 443 : 80)
155
+ @connection.use_ssl = master.use_ssl
156
+ @connection.verify_mode = OpenSSL::SSL::VERIFY_NONE if master.use_ssl
157
+ end
158
+
159
+ def post(path, body, headers = {})
160
+ request = Net::HTTP::Post.new(path, headers.merge('Accept' => 'application/xml'))
161
+ request.basic_auth(@master.user, @master.password)
162
+ @connection.request(request, body)
163
+ end
164
+
165
+ def get(path, headers = {})
166
+ request = Net::HTTP::Get.new(path, headers.merge('Accept' => 'application/xml'))
167
+ request.basic_auth(@master.user, @master.password)
168
+ @connection.request(request)
169
+ end
170
+ end
171
+
172
+ class Resource < ActiveResource::Base #:nodoc:
173
+ class << self
174
+ def parent_resources(*parents)
175
+ @parent_resources = parents
176
+ end
177
+
178
+ def element_name
179
+ name.split(/::/).last.underscore
180
+ end
181
+
182
+ def prefix_source
183
+ if @parent_resources
184
+ @parent_resources.map { |resource| "/#{resource.to_s.pluralize}/:#{resource}_id" }.join + '/'
185
+ else
186
+ '/'
187
+ end
188
+ end
189
+
190
+ def prefix(options = {})
191
+ if options.any?
192
+ options.map { |name, value| "/#{name.to_s.chomp('_id').pluralize}/#{value}" }.join + '/'
193
+ else
194
+ '/'
195
+ end
196
+ end
197
+ end
198
+
199
+ def prefix_options
200
+ id ? {} : super
201
+ end
202
+ end
203
+
204
+ class Project < Resource
205
+ end
206
+
207
+ class Company < Resource
208
+ parent_resources :project
209
+
210
+ def self.on_project(project_id, options = {})
211
+ find(:all, :params => options.merge(:project_id => project_id))
212
+ end
213
+ end
214
+
215
+ # == Creating different types of categories
216
+ #
217
+ # The type parameter is required when creating a category. For exampe, to
218
+ # create an attachment category for a particular project:
219
+ #
220
+ # c = Basecamp::Category.new(:project_id => 1037)
221
+ # c.type = 'attachment'
222
+ # c.name = 'Pictures'
223
+ # c.save # => true
224
+ #
225
+ class Category < Resource
226
+ parent_resources :project
227
+
228
+ def self.all(project_id, options = {})
229
+ find(:all, :params => options.merge(:project_id => project_id))
230
+ end
231
+
232
+ def self.post_categories(project_id, options = {})
233
+ find(:all, :params => options.merge(:project_id => project_id, :type => 'post'))
234
+ end
235
+
236
+ def self.attachment_categories(project_id, options = {})
237
+ find(:all, :params => options.merge(:project_id => project_id, :type => 'attachment'))
238
+ end
239
+ end
240
+
241
+ class Message < Resource
242
+ parent_resources :project
243
+ set_element_name 'post'
244
+
245
+ # Returns the most recent 25 messages in the given project (and category,
246
+ # if specified). If you need to retrieve older messages, use the archive
247
+ # method instead. Example:
248
+ #
249
+ # Basecamp::Message.recent(1037)
250
+ # Basecamp::Message.recent(1037, :category_id => 7301)
251
+ #
252
+ def self.recent(project_id, options = {})
253
+ find(:all, :params => options.merge(:project_id => project_id))
254
+ end
255
+
256
+ # Returns a summary of all messages in the given project (and category, if
257
+ # specified). The summary is simply the title and category of the message,
258
+ # as well as the number of attachments (if any). Example:
259
+ #
260
+ # Basecamp::Message.archive(1037)
261
+ # Basecamp::Message.archive(1037, :category_id => 7301)
262
+ #
263
+ def self.archive(project_id, options = {})
264
+ find(:all, :params => options.merge(:project_id => project_id), :from => :archive)
265
+ end
266
+
267
+ def comments(options = {})
268
+ @comments ||= Comment.find(:all, :params => options.merge(:post_id => id))
269
+ end
270
+ end
271
+
272
+ # == Creating comments for multiple resources
273
+ #
274
+ # Comments can be created for messages, milestones, and to-dos, identified
275
+ # by the <tt>post_id</tt>, <tt>milestone_id</tt>, and <tt>todo_item_id</tt>
276
+ # params respectively.
277
+ #
278
+ # For example, to create a comment on the message with id #8675309:
279
+ #
280
+ # c = Basecamp::Comment.new(:post_id => 8675309)
281
+ # c.body = 'Great tune'
282
+ # c.save # => true
283
+ #
284
+ # Similarly, to create a comment on a milestone:
285
+ #
286
+ # c = Basecamp::Comment.new(:milestone_id => 8473647)
287
+ # c.body = 'Is this done yet?'
288
+ # c.save # => true
289
+ #
290
+ class Comment < Resource
291
+ parent_resources :post, :milestone, :todo_item
292
+ end
293
+
294
+ class TodoList < Resource
295
+ parent_resources :project
296
+
297
+ # Returns all lists for a project. If complete is true, only completed lists
298
+ # are returned. If complete is false, only uncompleted lists are returned.
299
+ def self.all(project_id, complete = nil)
300
+ filter = case complete
301
+ when nil then "all"
302
+ when true then "finished"
303
+ when false then "pending"
304
+ else raise ArgumentError, "invalid value for `complete'"
305
+ end
306
+
307
+ find(:all, :params => { :project_id => project_id, :filter => filter })
308
+ end
309
+
310
+ def todo_items(options = {})
311
+ @todo_items ||= TodoItem.find(:all, :params => options.merge(:todo_list_id => id))
312
+ end
313
+ end
314
+
315
+ class TodoItem < Resource
316
+ parent_resources :todo_list
317
+
318
+ def todo_list(options = {})
319
+ @todo_list ||= TodoList.find(todo_list_id, options)
320
+ end
321
+
322
+ def time_entries(options = {})
323
+ @time_entries ||= TimeEntry.find(:all, :params => options.merge(:todo_item_id => id))
324
+ end
325
+
326
+ def comments(options = {})
327
+ @comments ||= Comment.find(:all, :params => options.merge(:todo_item_id => id))
328
+ end
329
+
330
+ def complete!
331
+ put(:complete)
332
+ end
333
+
334
+ def uncomplete!
335
+ put(:uncomplete)
336
+ end
337
+ end
338
+
339
+ class TimeEntry < Resource
340
+ parent_resources :project, :todo_item
341
+
342
+ def self.all(project_id, page = 0)
343
+ find(:all, :params => { :project_id => project_id, :page => page })
344
+ end
345
+
346
+ def self.report(options={})
347
+ find(:all, :from => :report, :params => options)
348
+ end
349
+ end
350
+
351
+ class Category < Resource
352
+ parent_resources :project
353
+ end
354
+
355
+ class Attachment
356
+ attr_accessor :id, :filename, :content
357
+
358
+ def self.create(filename, content)
359
+ returning new(filename, content) do |attachment|
360
+ attachment.save
361
+ end
362
+ end
363
+
364
+ def initialize(filename, content)
365
+ @filename, @content = filename, content
366
+ end
367
+
368
+ def attributes
369
+ { :file => id, :original_filename => filename }
370
+ end
371
+
372
+ def to_xml(options = {})
373
+ { :file => attributes }.to_xml(options)
374
+ end
375
+
376
+ def inspect
377
+ to_s
378
+ end
379
+
380
+ def save
381
+ response = Basecamp.connection.post('/upload', content, 'Content-Type' => 'application/octet-stream')
382
+
383
+ if response.code == '200'
384
+ self.id = Hash.from_xml(response.body)['upload']['id']
385
+ true
386
+ else
387
+ raise "Could not save attachment: #{response.message} (#{response.code})"
388
+ end
389
+ end
390
+ end
391
+
392
+ class Record #:nodoc:
393
+ attr_reader :type
394
+
395
+ def initialize(type, hash)
396
+ @type, @hash = type, hash
397
+ end
398
+
399
+ def [](name)
400
+ name = dashify(name)
401
+
402
+ case @hash[name]
403
+ when Hash then
404
+ @hash[name] = if (@hash[name].keys.length == 1 && @hash[name].values.first.is_a?(Array))
405
+ @hash[name].values.first.map { |v| Record.new(@hash[name].keys.first, v) }
406
+ else
407
+ Record.new(name, @hash[name])
408
+ end
409
+ else
410
+ @hash[name]
411
+ end
412
+ end
413
+
414
+ def id
415
+ @hash['id']
416
+ end
417
+
418
+ def attributes
419
+ @hash.keys
420
+ end
421
+
422
+ def respond_to?(sym)
423
+ super || @hash.has_key?(dashify(sym))
424
+ end
425
+
426
+ def method_missing(sym, *args)
427
+ if args.empty? && !block_given? && respond_to?(sym)
428
+ self[sym]
429
+ else
430
+ super
431
+ end
432
+ end
433
+
434
+ def to_s
435
+ "\#<Record(#{@type}) #{@hash.inspect[1..-2]}>"
436
+ end
437
+
438
+ def inspect
439
+ to_s
440
+ end
441
+
442
+ private
443
+
444
+ def dashify(name)
445
+ name.to_s.tr("_", "-")
446
+ end
447
+ end
448
+
449
+ attr_accessor :use_xml
450
+
451
+ class << self
452
+ attr_reader :site, :user, :password, :use_ssl
453
+
454
+ def establish_connection!(site, user, password, use_ssl = false)
455
+ @site = site
456
+ @user = user
457
+ @password = password
458
+ @use_ssl = use_ssl
459
+
460
+ Resource.user = user
461
+ Resource.password = password
462
+ Resource.site = (use_ssl ? "https" : "http") + "://" + site
463
+
464
+ @connection = Connection.new(self)
465
+ end
466
+
467
+ def connection
468
+ @connection || raise('No connection established')
469
+ end
470
+
471
+ def get_token
472
+ response = @connection.get('/me.xml')
473
+ xml = XmlSimple.xml_in(response.body)
474
+ xml['token'][0]
475
+ end
476
+ end
477
+
478
+ def initialize
479
+ @use_xml = false
480
+ end
481
+
482
+ # ==========================================================================
483
+ # PEOPLE
484
+ # ==========================================================================
485
+
486
+ # Return an array of the people in the given company. If the project-id is
487
+ # given, only people who have access to the given project will be returned.
488
+ def people(company_id, project_id=nil)
489
+ url = project_id ? "/projects/#{project_id}" : ""
490
+ url << "/contacts/people/#{company_id}"
491
+ records "person", url
492
+ end
493
+
494
+ # Return information about the person with the given id
495
+ def person(id)
496
+ record "/contacts/person/#{id}"
497
+ end
498
+
499
+ # ==========================================================================
500
+ # MILESTONES
501
+ # ==========================================================================
502
+
503
+ # Returns a list of all milestones for the given project, optionally filtered
504
+ # by whether they are completed, late, or upcoming.
505
+ def milestones(project_id, find = 'all')
506
+ records "milestone", "/projects/#{project_id}/milestones/list", :find => find
507
+ end
508
+
509
+ # Create a new milestone for the given project. +data+ must be hash of the
510
+ # values to set, including +title+, +deadline+, +responsible_party+, and
511
+ # +notify+.
512
+ def create_milestone(project_id, data)
513
+ create_milestones(project_id, [data]).first
514
+ end
515
+
516
+ # As #create_milestone, but can create multiple milestones in a single
517
+ # request. The +milestones+ parameter must be an array of milestone values as
518
+ # described in #create_milestone.
519
+ def create_milestones(project_id, milestones)
520
+ records "milestone", "/projects/#{project_id}/milestones/create", :milestone => milestones
521
+ end
522
+
523
+ # Updates an existing milestone.
524
+ def update_milestone(id, data, move = false, move_off_weekends = false)
525
+ record "/milestones/update/#{id}", :milestone => data,
526
+ :move_upcoming_milestones => move,
527
+ :move_upcoming_milestones_off_weekends => move_off_weekends
528
+ end
529
+
530
+ # Destroys the milestone with the given id.
531
+ def delete_milestone(id)
532
+ record "/milestones/delete/#{id}"
533
+ end
534
+
535
+ # Complete the milestone with the given id
536
+ def complete_milestone(id)
537
+ record "/milestones/complete/#{id}"
538
+ end
539
+
540
+ # Uncomplete the milestone with the given id
541
+ def uncomplete_milestone(id)
542
+ record "/milestones/uncomplete/#{id}"
543
+ end
544
+
545
+ private
546
+
547
+ # Make a raw web-service request to Basecamp. This will return a Hash of
548
+ # Arrays of the response, and may seem a little odd to the uninitiated.
549
+ def request(path, parameters = {})
550
+ response = Basecamp.connection.post(path, convert_body(parameters), "Content-Type" => content_type)
551
+
552
+ if response.code.to_i / 100 == 2
553
+ result = XmlSimple.xml_in(response.body, 'keeproot' => true, 'contentkey' => '__content__', 'forcecontent' => true)
554
+ typecast_value(result)
555
+ else
556
+ raise "#{response.message} (#{response.code})"
557
+ end
558
+ end
559
+
560
+ # A convenience method for wrapping the result of a query in a Record
561
+ # object. This assumes that the result is a singleton, not a collection.
562
+ def record(path, parameters={})
563
+ result = request(path, parameters)
564
+ (result && !result.empty?) ? Record.new(result.keys.first, result.values.first) : nil
565
+ end
566
+
567
+ # A convenience method for wrapping the result of a query in Record
568
+ # objects. This assumes that the result is a collection--any singleton
569
+ # result will be wrapped in an array.
570
+ def records(node, path, parameters={})
571
+ result = request(path, parameters).values.first or return []
572
+ result = result[node] or return []
573
+ result = [result] unless Array === result
574
+ result.map { |row| Record.new(node, row) }
575
+ end
576
+
577
+ def convert_body(body)
578
+ body = use_xml ? body.to_legacy_xml : body.to_yaml
579
+ end
580
+
581
+ def content_type
582
+ use_xml ? "application/xml" : "application/x-yaml"
583
+ end
584
+
585
+ def typecast_value(value)
586
+ case value
587
+ when Hash
588
+ if value.has_key?("__content__")
589
+ content = translate_entities(value["__content__"]).strip
590
+ case value["type"]
591
+ when "integer" then content.to_i
592
+ when "boolean" then content == "true"
593
+ when "datetime" then Time.parse(content)
594
+ when "date" then Date.parse(content)
595
+ else content
596
+ end
597
+ # a special case to work-around a bug in XmlSimple. When you have an empty
598
+ # tag that has an attribute, XmlSimple will not add the __content__ key
599
+ # to the returned hash. Thus, we check for the presense of the 'type'
600
+ # attribute to look for empty, typed tags, and simply return nil for
601
+ # their value.
602
+ elsif value.keys == %w(type)
603
+ nil
604
+ elsif value["nil"] == "true"
605
+ nil
606
+ # another special case, introduced by the latest rails, where an array
607
+ # type now exists. This is parsed by XmlSimple as a two-key hash, where
608
+ # one key is 'type' and the other is the actual array value.
609
+ elsif value.keys.length == 2 && value["type"] == "array"
610
+ value.delete("type")
611
+ typecast_value(value)
612
+ else
613
+ value.empty? ? nil : value.inject({}) do |h,(k,v)|
614
+ h[k] = typecast_value(v)
615
+ h
616
+ end
617
+ end
618
+ when Array
619
+ value.map! { |i| typecast_value(i) }
620
+ case value.length
621
+ when 0 then nil
622
+ when 1 then value.first
623
+ else value
624
+ end
625
+ else
626
+ raise "can't typecast #{value.inspect}"
627
+ end
628
+ end
629
+
630
+ def translate_entities(value)
631
+ value.gsub(/&lt;/, "<").
632
+ gsub(/&gt;/, ">").
633
+ gsub(/&quot;/, '"').
634
+ gsub(/&apos;/, "'").
635
+ gsub(/&amp;/, "&")
636
+ end
637
+ end
638
+
639
+ # A minor hack to let Xml-Simple serialize symbolic keys in hashes
640
+ class Symbol
641
+ def [](*args)
642
+ to_s[*args]
643
+ end
644
+ end
645
+
646
+ class Hash
647
+ def to_legacy_xml
648
+ XmlSimple.xml_out({:request => self}, 'keeproot' => true, 'noattr' => true)
649
+ end
650
+ end
data/readme ADDED
@@ -0,0 +1,13 @@
1
+ = Using the Basecamp API with Ruby
2
+
3
+ This is the basecamp wrapper provided by 37Signals to access their API with a little change to obtain the API token.
4
+
5
+ This is useful when you only have username/password and you want to obtain the token automatically to use that in future calls.
6
+
7
+ = Usage
8
+
9
+ >> Basecamp.establish_connection!('<subdomain>.basecamphq.com', <username>, <password>, true)
10
+ >> Basecamp.get_token
11
+ => "the token"
12
+
13
+ -- Anibal Cucco
metadata ADDED
@@ -0,0 +1,146 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: basecamp
3
+ version: !ruby/object:Gem::Version
4
+ hash: 27
5
+ prerelease: false
6
+ segments:
7
+ - 0
8
+ - 0
9
+ - 2
10
+ version: 0.0.2
11
+ platform: ruby
12
+ authors:
13
+ - Anibal Cucco
14
+ - James A. Rosen
15
+ autorequire:
16
+ bindir: bin
17
+ cert_chain: []
18
+
19
+ date: 2010-06-15 00:00:00 -04:00
20
+ default_executable:
21
+ dependencies:
22
+ - !ruby/object:Gem::Dependency
23
+ name: oauth2
24
+ prerelease: false
25
+ requirement: &id001 !ruby/object:Gem::Requirement
26
+ none: false
27
+ requirements:
28
+ - - ~>
29
+ - !ruby/object:Gem::Version
30
+ hash: 15
31
+ segments:
32
+ - 0
33
+ - 0
34
+ - 8
35
+ version: 0.0.8
36
+ type: :runtime
37
+ version_requirements: *id001
38
+ - !ruby/object:Gem::Dependency
39
+ name: rake
40
+ prerelease: false
41
+ requirement: &id002 !ruby/object:Gem::Requirement
42
+ none: false
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ hash: 3
47
+ segments:
48
+ - 0
49
+ version: "0"
50
+ type: :development
51
+ version_requirements: *id002
52
+ - !ruby/object:Gem::Dependency
53
+ name: mg
54
+ prerelease: false
55
+ requirement: &id003 !ruby/object:Gem::Requirement
56
+ none: false
57
+ requirements:
58
+ - - ~>
59
+ - !ruby/object:Gem::Version
60
+ hash: 15
61
+ segments:
62
+ - 0
63
+ - 0
64
+ - 8
65
+ version: 0.0.8
66
+ type: :development
67
+ version_requirements: *id003
68
+ - !ruby/object:Gem::Dependency
69
+ name: rspec
70
+ prerelease: false
71
+ requirement: &id004 !ruby/object:Gem::Requirement
72
+ none: false
73
+ requirements:
74
+ - - ~>
75
+ - !ruby/object:Gem::Version
76
+ hash: 27
77
+ segments:
78
+ - 1
79
+ - 3
80
+ - 0
81
+ version: 1.3.0
82
+ type: :development
83
+ version_requirements: *id004
84
+ - !ruby/object:Gem::Dependency
85
+ name: webmock
86
+ prerelease: false
87
+ requirement: &id005 !ruby/object:Gem::Requirement
88
+ none: false
89
+ requirements:
90
+ - - ~>
91
+ - !ruby/object:Gem::Version
92
+ hash: 27
93
+ segments:
94
+ - 1
95
+ - 2
96
+ - 2
97
+ version: 1.2.2
98
+ type: :development
99
+ version_requirements: *id005
100
+ description: Basecamp API wrapper.
101
+ email: nobody@gmail.com
102
+ executables: []
103
+
104
+ extensions: []
105
+
106
+ extra_rdoc_files: []
107
+
108
+ files:
109
+ - lib/basecamp.rb
110
+ - readme
111
+ has_rdoc: true
112
+ homepage: http://github.com/anibalcucco/basecamp-wrapper
113
+ licenses: []
114
+
115
+ post_install_message:
116
+ rdoc_options: []
117
+
118
+ require_paths:
119
+ - lib
120
+ required_ruby_version: !ruby/object:Gem::Requirement
121
+ none: false
122
+ requirements:
123
+ - - ">="
124
+ - !ruby/object:Gem::Version
125
+ hash: 3
126
+ segments:
127
+ - 0
128
+ version: "0"
129
+ required_rubygems_version: !ruby/object:Gem::Requirement
130
+ none: false
131
+ requirements:
132
+ - - ">="
133
+ - !ruby/object:Gem::Version
134
+ hash: 3
135
+ segments:
136
+ - 0
137
+ version: "0"
138
+ requirements: []
139
+
140
+ rubyforge_project:
141
+ rubygems_version: 1.3.7
142
+ signing_key:
143
+ specification_version: 3
144
+ summary: Basecamp API wrapper.
145
+ test_files: []
146
+