carbon 0.2.4 → 0.2.5

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.
Binary file
@@ -1,7 +1,10 @@
1
1
  require 'uri'
2
2
  require 'blockenspiel'
3
- require 'rest' # provided by nap gem
4
3
  require 'timeframe'
4
+ require 'digest/sha1'
5
+ require 'rest' # provided by nap gem
6
+ gem 'SystemTimer' # >=1.2, so as not to be confused with system_timer 1.0
7
+ require 'system_timer'
5
8
  require 'active_support/version'
6
9
  %w{
7
10
  active_support/core_ext/module/attribute_accessors
@@ -44,6 +47,7 @@ module Carbon
44
47
 
45
48
  REALTIME_URL = 'http://carbon.brighterplanet.com'
46
49
  ASYNC_URL = 'https://queue.amazonaws.com/121562143717/cm1_production_incoming'
50
+ STORAGE_URL = 'http://storage.carbon.brighterplanet.com'
47
51
 
48
52
  class RealtimeEstimateFailed < RuntimeError # :nodoc:
49
53
  end
@@ -1,5 +1,6 @@
1
1
  require 'carbon/emission_estimate/response'
2
2
  require 'carbon/emission_estimate/request'
3
+ require 'carbon/emission_estimate/storage'
3
4
 
4
5
  module Carbon
5
6
  # Let's start off by saying that realtime <tt>EmissionEstimate</tt> objects quack like numbers.
@@ -10,11 +11,18 @@ module Carbon
10
11
  #
11
12
  # At the same time, they contain all the data you get back from the emission estimate web service. For example, you could say <tt>puts my_donut_factor.emission_estimate.oven_count</tt> (see the tests) and you'd get back the oven count used in the calculation, if any.
12
13
  class EmissionEstimate
14
+ def self.parse(str)
15
+ data = ::ActiveSupport::JSON.decode str
16
+ data['active_subtimeframe'] = ::Timeframe.interval(data['active_subtimeframe']) if data.has_key? 'active_subtimeframe'
17
+ data['updated_at'] = ::Time.parse(data['updated_at']) if data.has_key? 'updated_at'
18
+ data
19
+ end
20
+
13
21
  def initialize(emitter)
14
22
  @emitter = emitter
15
23
  end
16
24
 
17
- VALID_OPTIONS = [:callback_content_type, :key, :callback, :timeframe]
25
+ VALID_OPTIONS = [:callback_content_type, :key, :callback, :timeframe, :guid, :timeout, :defer]
18
26
  def take_options(options)
19
27
  return if options.blank?
20
28
  options.slice(*VALID_OPTIONS).each do |k, v|
@@ -25,7 +33,7 @@ module Carbon
25
33
  # I can be compared directly to a number, unless I'm an async request.
26
34
  def ==(other)
27
35
  if other.is_a? ::Numeric and mode == :realtime
28
- other == response.number
36
+ other == number
29
37
  else
30
38
  super
31
39
  end
@@ -38,23 +46,35 @@ module Carbon
38
46
  # > my_car.emission_estimate.model
39
47
  # => 'Ford Taurus'
40
48
  def method_missing(method_id, *args, &blk)
41
- if !block_given? and args.empty? and response.data.has_key? method_id.to_s
42
- response.data[method_id.to_s]
49
+ if !block_given? and args.empty? and data.has_key? method_id.to_s
50
+ data[method_id.to_s]
43
51
  elsif ::Float.method_defined? method_id
44
52
  raise TriedToUseAsyncResponseAsNumber if mode == :async
45
- response.number.send method_id, *args, &blk
53
+ number.send method_id, *args, &blk
46
54
  else
47
55
  super
48
56
  end
49
57
  end
50
-
51
58
  attr_writer :callback_content_type
52
59
  attr_writer :key
53
-
60
+ attr_writer :timeout
61
+ attr_writer :defer
54
62
  attr_accessor :callback
55
63
  attr_accessor :timeframe
56
-
64
+ attr_accessor :guid
57
65
  attr_reader :emitter
66
+ def data
67
+ if storage.present?
68
+ storage.data
69
+ else
70
+ response.data
71
+ end
72
+ end
73
+ def storage
74
+ @storage ||= {}
75
+ return @storage[guid] if @storage.has_key? guid
76
+ @storage[guid] = Storage.new self
77
+ end
58
78
  def request
59
79
  @request ||= Request.new self
60
80
  end
@@ -65,8 +85,18 @@ module Carbon
65
85
  return @response[current_params] if @response.has_key? current_params
66
86
  @response[current_params] = Response.new self
67
87
  end
88
+ def defer?
89
+ @defer == true
90
+ end
91
+ def async?
92
+ callback or defer?
93
+ end
68
94
  def mode
69
- callback ? :async : :realtime
95
+ async? ? :async : :realtime
96
+ end
97
+ # Timeout on realtime requests in seconds, if desired.
98
+ def timeout
99
+ @timeout
70
100
  end
71
101
  def callback_content_type
72
102
  @callback_content_type || 'application/json'
@@ -76,27 +106,27 @@ module Carbon
76
106
  end
77
107
  # The timeframe being looked at in the emission calculation.
78
108
  def active_subtimeframe
79
- response.data['active_subtimeframe']
109
+ data['active_subtimeframe']
80
110
  end
81
111
  # Another way to access the emission value.
82
112
  # Useful if you don't like treating <tt>EmissionEstimate</tt> objects like <tt>Numeric</tt> objects (even though they do quack like numbers...)
83
- def emission_value
84
- response.number
113
+ def number
114
+ async? ? nil : data['emission'].to_f.freeze
85
115
  end
86
116
  # The units of the emission.
87
117
  def emission_units
88
- response.data['emission_units']
118
+ data['emission_units']
89
119
  end
90
120
  # Errors (and warnings) as reported in the response.
91
121
  # Note: may contain HTML tags like KBD or A
92
122
  def errors
93
- response.data['errors']
123
+ data['errors']
94
124
  end
95
125
  # The URL of the methodology report indicating how this estimate was calculated.
96
126
  # > my_car.emission_estimate.methodology
97
127
  # => 'http://carbon.brighterplanet.com/automobiles.html?[...]'
98
128
  def methodology
99
- response.data['methodology']
129
+ data['methodology']
100
130
  end
101
131
  end
102
132
  end
@@ -12,10 +12,13 @@ module Carbon
12
12
  send "#{parent.mode}_params"
13
13
  end
14
14
  def async_params # :nodoc:
15
+ raise ::ArgumentError, "When using :callback you cannot specify :defer" if parent.defer? and parent.callback
16
+ raise ::ArgumentError, "When using :defer => true you must specify :guid" if parent.defer? and parent.guid.blank?
15
17
  hash = _params
16
18
  hash[:emitter] = parent.emitter.class.carbon_base.emitter_common_name
17
- hash[:callback] = parent.callback
18
- hash[:callback_content_type] = parent.callback_content_type
19
+ hash[:callback] = parent.callback if parent.callback
20
+ hash[:callback_content_type] = parent.callback_content_type if parent.callback
21
+ hash[:guid] = parent.guid if parent.defer?
19
22
  {
20
23
  :Action => 'SendMessage',
21
24
  :Version => '2009-02-01',
@@ -3,7 +3,6 @@ module Carbon
3
3
  class Response
4
4
  attr_reader :parent
5
5
  attr_reader :data
6
- attr_reader :number
7
6
  attr_reader :raw_request
8
7
  attr_reader :raw_response
9
8
  def initialize(parent)
@@ -12,6 +11,7 @@ module Carbon
12
11
  end
13
12
  def load_realtime_data # :nodoc:
14
13
  attempts = 0
14
+ response = nil
15
15
  begin
16
16
  response = perform
17
17
  raise ::Carbon::RateLimited if response.status_code == 403 and response.body =~ /Rate Limit/i
@@ -21,26 +21,31 @@ module Carbon
21
21
  sleep 0.2 * attempts
22
22
  retry
23
23
  else
24
- raise $!, "Rate limited #{attempts} time(s) in a row"
24
+ raise $!
25
25
  end
26
26
  end
27
27
  raise ::Carbon::RealtimeEstimateFailed unless response.success?
28
- @data = ::ActiveSupport::JSON.decode response.body
29
- instantiate_known_response_objects
30
- @number = data['emission'].to_f.freeze
31
- end
32
- def instantiate_known_response_objects # :nodoc:
33
- data['active_subtimeframe'] = ::Timeframe.interval(data['active_subtimeframe']) if data.has_key? 'active_subtimeframe'
28
+ @data = ::Carbon::EmissionEstimate.parse response.body
34
29
  end
35
30
  def load_async_data # :nodoc:
36
31
  response = perform
37
32
  raise ::Carbon::QueueingFailed unless response.success?
38
33
  @data = {}
39
- @number = nil
40
34
  end
41
35
  def perform # :nodoc:
36
+ response = nil
37
+ if parent.timeout
38
+ ::SystemTimer.timeout_after(parent.timeout) do
39
+ response = _perform
40
+ end
41
+ else
42
+ response = _perform
43
+ end
44
+ response
45
+ end
46
+ def _perform # :nodoc:
42
47
  @raw_request = ::REST::Request.new :post, ::URI.parse(parent.request.url), parent.request.body, {'Content-Type' => 'application/x-www-form-urlencoded; charset=utf-8'}
43
- @raw_response = @raw_request.perform
48
+ @raw_response = raw_request.perform
44
49
  end
45
50
  end
46
51
  end
@@ -0,0 +1,29 @@
1
+ module Carbon
2
+ class EmissionEstimate
3
+ class Storage
4
+ attr_accessor :parent
5
+ attr_reader :raw_request
6
+ attr_reader :raw_response
7
+ def initialize(parent)
8
+ @parent = parent
9
+ end
10
+ def url
11
+ "#{::Carbon::STORAGE_URL}/#{::Digest::SHA1.hexdigest(parent.key+parent.guid)}"
12
+ end
13
+ def present?
14
+ parent.guid.present? and data.present?
15
+ end
16
+ def data
17
+ return @data[0] if @data.is_a? ::Array
18
+ @raw_request = ::REST::Request.new :get, ::URI.parse(url)
19
+ @raw_response = raw_request.perform
20
+ if raw_response.success?
21
+ @data = [::Carbon::EmissionEstimate.parse(raw_response.body)]
22
+ else
23
+ @data = []
24
+ end
25
+ @data[0]
26
+ end
27
+ end
28
+ end
29
+ end
@@ -1,6 +1,85 @@
1
1
  require 'spec_helper'
2
+ require 'active_support/json/encoding'
3
+ require 'fakeweb'
2
4
 
3
5
  CALLBACK_URL = 'http://www.postbin.org/1dj0146'
6
+ KEY = 'valid'
7
+ EXISTING_UNIQUE_ID = 'oisjoaioijodijaosijdoias'
8
+ MISSING_UNIQUE_ID = 'd09joijdoijaloijdoais'
9
+ OTHER_UNIQUE_ID = '1092fjoid;oijsao;ga'
10
+
11
+ FakeWeb.register_uri :post,
12
+ /carbon.brighterplanet.com.automobiles/,
13
+ :status => ["200", "OK"],
14
+ :body => {
15
+ 'emission' => '134.599',
16
+ 'emission_units' => 'kilograms',
17
+ 'methodology' => 'http://carbon.brighterplanet.com/something',
18
+ 'active_subtimeframe' => Timeframe.new(:year => 2008)
19
+ }.to_json
20
+ #
21
+ FakeWeb.register_uri :post,
22
+ /carbon.brighterplanet.com.factories/,
23
+ :status => ["200", "OK"],
24
+ :body => {
25
+ 'emission' => 1000.0,
26
+ 'emission_units' => 'kilograms',
27
+ 'methodology' => 'http://carbon.brighterplanet.com/something',
28
+ 'active_subtimeframe' => Timeframe.new(:year => 2008)
29
+ }.to_json
30
+ #
31
+ FakeWeb.register_uri :post,
32
+ /queue.amazonaws.com/,
33
+ :status => ["200", "OK"],
34
+ :body => 'You would see an amazon aws response'
35
+ # yep, it's stored!
36
+ FakeWeb.register_uri :get,
37
+ "http://storage.carbon.brighterplanet.com/#{Digest::SHA1.hexdigest(KEY+EXISTING_UNIQUE_ID)}",
38
+ :status => ["200", "OK"],
39
+ :body => {
40
+ 'emission' => 1234,
41
+ 'emission_units' => 'kilograms',
42
+ 'methodology' => 'http://carbon.brighterplanet.com/something',
43
+ 'updated_at' => Time.now.as_json
44
+ }.to_json
45
+ #
46
+ FakeWeb.register_uri :get,
47
+ "http://storage.carbon.brighterplanet.com/#{Digest::SHA1.hexdigest(KEY+OTHER_UNIQUE_ID)}",
48
+ :status => ["200", "OK"],
49
+ :body => {
50
+ 'emission' => 99982,
51
+ 'emission_units' => 'kilograms',
52
+ 'methodology' => 'http://carbon.brighterplanet.com/something',
53
+ 'updated_at' => Time.now.as_json
54
+ }.to_json
55
+ #
56
+ FakeWeb.register_uri :get,
57
+ "http://storage.carbon.brighterplanet.com/#{Digest::SHA1.hexdigest(KEY+MISSING_UNIQUE_ID)}",
58
+ [
59
+ {
60
+ :status => ["404", "Not Found"],
61
+ :body => "It's not here, you better ask carbon for it!"
62
+ },
63
+ {
64
+ :status => ["200", "OK"],
65
+ :body => {
66
+ 'emission' => 9876,
67
+ 'emission_units' => 'kilograms',
68
+ 'methodology' => 'http://carbon.brighterplanet.com/something',
69
+ 'updated_at' => Time.now.as_json
70
+ }.to_json
71
+ }
72
+ ]
73
+ #
74
+ # FakeWeb.register_uri :post,
75
+ # /carbon.brighterplanet.com.*#{MISSING_UNIQUE_ID}/,
76
+ # :status => ["302", "Moved Permanently"],
77
+ # :body => {
78
+ # 'emission' => 9876,
79
+ # 'emission_units' => 'kilograms',
80
+ # 'methodology' => 'http://carbon.brighterplanet.com/something'
81
+ # }.to_json,
82
+ # :headers => { 'Location' => "http://storage.carbon.brighterplanet.com/#{Digest::SHA1.hexdigest(KEY+MISSING_UNIQUE_ID)}" }
4
83
 
5
84
  class RentalCar
6
85
  include Carbon
@@ -45,9 +124,24 @@ class DonutFactory
45
124
  end
46
125
  end
47
126
 
127
+ # set up timeouts
128
+ module Carbon
129
+ class EmissionEstimate
130
+ attr_accessor :sleep_before_performing
131
+ VALID_OPTIONS.push :sleep_before_performing
132
+ class Response
133
+ def _perform_with_delay
134
+ sleep parent.sleep_before_performing if parent.sleep_before_performing
135
+ _perform_without_delay
136
+ end
137
+ alias_method_chain :_perform, :delay
138
+ end
139
+ end
140
+ end
141
+
48
142
  describe Carbon do
49
143
  before(:each) do
50
- Carbon.key = 'valid'
144
+ Carbon.key = KEY
51
145
  end
52
146
 
53
147
  it 'should be simple to use' do
@@ -104,6 +198,52 @@ describe Carbon do
104
198
  end
105
199
  end
106
200
 
201
+ describe 'requests that can be stored (cached) by guid' do
202
+ it 'should find existing unique ids on S3' do
203
+ d = DonutFactory.new
204
+ d.emission_estimate(:guid => EXISTING_UNIQUE_ID).should == 1234
205
+ d.emission_estimate(:guid => EXISTING_UNIQUE_ID).updated_at.should be_instance_of(Time)
206
+ end
207
+ it "should pass through to realtime if unique id isn't found on S3" do
208
+ d = DonutFactory.new
209
+ d.emission_estimate(:guid => MISSING_UNIQUE_ID).should == 1000
210
+ d.emission_estimate(:guid => MISSING_UNIQUE_ID).response.data.keys.should_not include('updated_at')
211
+ d1 = DonutFactory.new
212
+ d1.emission_estimate(:guid => MISSING_UNIQUE_ID).should == 9876
213
+ d1.emission_estimate(:guid => MISSING_UNIQUE_ID).updated_at.should be_instance_of(Time)
214
+ end
215
+ it "should depend on the user to update the guid if they want a new estimate" do
216
+ d = DonutFactory.new
217
+ d.oven_count = 12_000
218
+ str1 = d.emission_estimate(:guid => EXISTING_UNIQUE_ID).methodology
219
+ str1.should equal(d.emission_estimate(:guid => EXISTING_UNIQUE_ID).methodology)
220
+ d.oven_count = 13_000
221
+ str1.should equal(d.emission_estimate(:guid => EXISTING_UNIQUE_ID).methodology)
222
+ str1.should_not equal(d.emission_estimate(:guid => OTHER_UNIQUE_ID).methodology)
223
+ end
224
+ it 'should be deferrable for use in 2-pass reporting systems' do
225
+ d = DonutFactory.new
226
+ d.emission_estimate(:guid => EXISTING_UNIQUE_ID, :defer => true).number.should be_nil
227
+ d.emission_estimate(:guid => EXISTING_UNIQUE_ID, :defer => true).request.url.should =~ /amazonaws/
228
+ end
229
+ it 'should complain if you provide defer but not guid' do
230
+ d = DonutFactory.new
231
+ lambda {
232
+ d.emission_estimate(:defer => true).request.params
233
+ }.should raise_error(ArgumentError, /defer.*guid/i)
234
+ end
235
+ it 'should complain if you provide guid and callback' do
236
+ d = DonutFactory.new
237
+ lambda {
238
+ d.emission_estimate(:defer => true, :callback => 'foobar').request.params
239
+ }.should raise_error(ArgumentError, /callback.*defer/i)
240
+ end
241
+ it 'should send guid along with other parameters when queueing up deferred request' do
242
+ d = DonutFactory.new
243
+ d.emission_estimate(:guid => EXISTING_UNIQUE_ID, :defer => true).request.params[:MessageBody].should =~ /#{EXISTING_UNIQUE_ID.to_query(:guid)}/
244
+ end
245
+ end
246
+
107
247
  describe 'synchronous (realtime) requests' do
108
248
  it 'should send simple params' do
109
249
  d = DonutFactory.new
@@ -138,10 +278,17 @@ describe Carbon do
138
278
  it 'should override defaults' do
139
279
  d = DonutFactory.new
140
280
  key = 'ADifferentOne'
141
- d.emission_estimate.key.should == 'valid'
281
+ d.emission_estimate.key.should == KEY
142
282
  d.emission_estimate.key = key
143
283
  d.emission_estimate.key.should == key
144
284
  end
285
+
286
+ it 'should accept timeouts' do
287
+ d = DonutFactory.new
288
+ lambda {
289
+ d.emission_estimate(:sleep_before_performing => 2, :timeout => 1).to_f
290
+ }.should raise_error(::Timeout::Error)
291
+ end
145
292
 
146
293
  it 'should accept timeframes' do
147
294
  c = RentalCar.new
@@ -180,7 +327,7 @@ describe Carbon do
180
327
  it 'should have nil data in its response' do
181
328
  c = RentalCar.new
182
329
  c.emission_estimate.callback = CALLBACK_URL
183
- c.emission_estimate.emission_value.should be_nil
330
+ c.emission_estimate.number.should be_nil
184
331
  c.emission_estimate.emission_units.should be_nil
185
332
  c.emission_estimate.methodology.should be_nil
186
333
  end