carbon 1.1.3 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. data/.gitignore +4 -22
  2. data/CHANGELOG +11 -0
  3. data/Gemfile +10 -1
  4. data/README.markdown +185 -0
  5. data/Rakefile +13 -26
  6. data/bin/carbon +3 -3
  7. data/carbon.gemspec +17 -23
  8. data/developer_notes/MULTI.markdown +25 -0
  9. data/developer_notes/REDUCE_HTTP_CONNECTIONS.markdown +46 -0
  10. data/features/shell.feature +1 -1
  11. data/features/support/env.rb +3 -4
  12. data/lib/carbon.rb +242 -96
  13. data/lib/carbon/registry.rb +50 -0
  14. data/lib/carbon/shell.rb +14 -8
  15. data/lib/carbon/shell/emitter.rb +33 -29
  16. data/lib/carbon/version.rb +1 -1
  17. data/test/carbon_test.rb +167 -0
  18. metadata +128 -182
  19. data/MIT-LICENSE.txt +0 -19
  20. data/README.rdoc +0 -266
  21. data/doc/INTEGRATION_GUIDE.rdoc +0 -1002
  22. data/doc/examining-response-with-jsonview.png +0 -0
  23. data/doc/shell_example +0 -43
  24. data/doc/timeout-error.png +0 -0
  25. data/doc/with-committee-reports.png +0 -0
  26. data/doc/without-committee-reports.png +0 -0
  27. data/lib/carbon/base.rb +0 -62
  28. data/lib/carbon/emission_estimate.rb +0 -165
  29. data/lib/carbon/emission_estimate/request.rb +0 -100
  30. data/lib/carbon/emission_estimate/response.rb +0 -61
  31. data/lib/carbon/emission_estimate/storage.rb +0 -33
  32. data/spec/fixtures/vcr_cassettes/flight.yml +0 -47
  33. data/spec/fixtures/vcr_cassettes/residence.yml +0 -44
  34. data/spec/lib/carbon/emission_estimate/request_spec.rb +0 -41
  35. data/spec/lib/carbon/emission_estimate/response_spec.rb +0 -33
  36. data/spec/lib/carbon/emission_estimate_spec.rb +0 -32
  37. data/spec/lib/carbon_spec.rb +0 -384
  38. data/spec/spec_helper.rb +0 -60
  39. data/spec/specwatchr +0 -60
data/lib/carbon.rb CHANGED
@@ -1,115 +1,261 @@
1
- require 'uri'
2
- require 'blockenspiel'
3
- require 'timeframe'
4
- require 'digest/sha1'
5
- require 'rest' # provided by nap gem
6
- require 'active_support'
7
- require 'active_support/version'
8
- %w{
9
- active_support/core_ext
10
- active_support/inflector
11
- active_support/inflector/inflections
12
- active_support/json/decoding
13
- }.each do |active_support_3_requirement|
14
- require active_support_3_requirement
15
- end if ActiveSupport::VERSION::MAJOR >= 3
1
+ require 'net/http'
2
+ require 'hashie/mash'
3
+ require 'multi_json'
4
+ require 'active_support/core_ext'
16
5
 
17
- require 'logger'
6
+ require 'carbon/registry'
18
7
 
19
- # A module (aka mixin) that lets you estimate carbon emissions by querying the {Brighter Planet carbon middleware emission estimate web service}[http://carbon.brighterplanet.com].
20
- #
21
- # class RentalCar
22
- # include Carbon
23
- # [...]
24
- # emit_as :automobile do
25
- # provide :make
26
- # provide :model
27
- # provide :model_year
28
- # end
29
- # end
30
- #
31
- # The DSL consists of the methods <tt>emit_as</tt> and <tt>provide</tt>.
32
- #
33
- # In this example, the DSL says:
34
- # * A rental car emits carbon like an "automobile", which is one of Brighter Planet's recognized emitter classes.
35
- # * Your implementation can provide up to three data points about a rental car: its make, its model, and its model year (but not necessarily all of them, all the time.)
36
- #
37
- # Once you've mixed in <tt>Carbon</tt>, you get the method <tt>emission_estimate</tt>, which you can call at any time to request an emission estimate.
38
8
  module Carbon
39
- autoload :Base, 'carbon/base'
40
- autoload :EmissionEstimate, 'carbon/emission_estimate'
41
- autoload :Registry, 'carbon/registry'
42
-
43
- def self.included(klass) # :nodoc:
44
- klass.extend ClassMethods
45
- end
46
-
47
- class RealtimeEstimateFailed < RuntimeError # :nodoc:
48
- end
49
- class QueueingFailed < RuntimeError # :nodoc:
50
- end
51
- class RateLimited < RuntimeError # :nodoc:
52
- end
53
- class TriedToUseAsyncResponseAsNumber < RuntimeError # :nodoc:
54
- end
9
+ DOMAIN = 'http://impact.brighterplanet.com'
55
10
 
56
- # The api key obtained from http://keys.brighterplanet.com
57
- mattr_accessor :key
11
+ # @private
12
+ # Make sure there are no warnings about class vars.
13
+ @@key = nil unless defined?(@@key)
58
14
 
59
- mattr_accessor :log
15
+ # Set the Brighter Planet API key that you can get from http://keys.brighterplanet.com
16
+ #
17
+ # @param [String] key The alphanumeric key.
18
+ #
19
+ # @return [nil]
20
+ def self.key=(key)
21
+ @@key = key
22
+ end
60
23
 
61
- def self.log #:nodoc:
62
- @log ||= Logger.new STDOUT
24
+ # Get the key you've set.
25
+ #
26
+ # @return [String] The key you set.
27
+ def self.key
28
+ @@key
63
29
  end
64
-
65
- def self.warn(msg) #:nodoc:
66
- log.warn msg
30
+
31
+ # Do a simple query.
32
+ #
33
+ # See the {file:README.html#API_response section about API responses} for an explanation of +Hashie::Mash+.
34
+ #
35
+ # @param [String] emitter The {http://impact.brighterplanet.com/emitters.json camelcased emitter name}.
36
+ # @param [Hash] params Characteristics, your API key (if you didn't set it globally), timeframe, compliance, etc.
37
+ #
38
+ # @option params [Timeframe] :timeframe (Timeframe.this_year) What time period to focus the calculation on. See {https://github.com/rossmeissl/timeframe timeframe} documentation.
39
+ # @option params [Array<Symbol>] :comply ([]) What {http://impact.brighterplanet.com/protocols.json calculation protocols} to require.
40
+ # @option params [String, Numeric] _characteristic_ Pieces of data about an emitter. The {http://impact.brighterplanet.com/flights/options Flight characteristics API} lists valid keys like +:aircraft+, +:origin_airport+, etc.
41
+ #
42
+ # @return [Hashie::Mash] An {file:README.html#API_response API response as documented in the README}
43
+ #
44
+ # @example A flight taken in 2009
45
+ # Carbon.query('Flight', :origin_airport => 'MSN', :destination_airport => 'ORD', :date => '2009-01-01', :timeframe => Timeframe.new(:year => 2009), :comply => [:tcr])
46
+ def self.query(emitter, params = {})
47
+ params ||= {}
48
+ params = params.reverse_merge(:key => key) if key
49
+ uri = ::URI.parse("#{DOMAIN}/#{emitter.underscore.pluralize}.json")
50
+ raw_response = ::Net::HTTP.post_form(uri, params)
51
+ response = ::Hashie::Mash.new
52
+ case raw_response
53
+ when ::Net::HTTPSuccess
54
+ response.status = raw_response.code.to_i
55
+ response.success = true
56
+ response.merge! ::MultiJson.decode(raw_response.body)
57
+ else
58
+ response.status = raw_response.code.to_i
59
+ response.success = false
60
+ response.error_body = raw_response.respond_to?(:body) ? raw_response.body : ''
61
+ response.errors = [raw_response.class.name]
62
+ end
63
+ response
67
64
  end
68
-
69
- # You will probably never access this module directly. Instead, you'll use it through the DSL.
65
+
66
+ # Perform many queries in parallel. Can be >90% faster than doing them serially (one after the other).
70
67
  #
71
- # It's mixed into any class that includes <tt>Carbon</tt>.
68
+ # See the {file:README.html#API_response section about API responses} for an explanation of +Hashie::Mash+.
69
+ #
70
+ # @param [Array<Array>] queries Multiple queries like you would pass to {Carbon.query}
71
+ #
72
+ # @return [Array<Hashie::Mash>] An array of {file:README.html#API_response API responses} in the same order as the queries.
73
+ #
74
+ # @note Not supported on JRuby because it uses {https://github.com/igrigorik/em-http-request em-http-request}, which suffers from {https://github.com/eventmachine/eventmachine/issues/155 an issue with +pending_connect_timeout+}.
75
+ #
76
+ # @example Two flights and an automobile trip
77
+ # queries = [
78
+ # ['Flight', :origin_airport => 'MSN', :destination_airport => 'ORD', :date => '2009-01-01', :timeframe => Timeframe.new(:year => 2009), :comply => [:tcr]],
79
+ # ['Flight', :origin_airport => 'SFO', :destination_airport => 'LAX', :date => '2011-09-29', :timeframe => Timeframe.new(:year => 2011), :comply => [:iso]],
80
+ # ['AutomobileTrip', :make => 'Nissan', :model => 'Altima', :timeframe => Timeframe.new(:year => 2008), :comply => [:tcr]]
81
+ # ]
82
+ # Carbon.multi(queries)
83
+ def self.multi(queries)
84
+ require 'em-http-request'
85
+ unsorted = {}
86
+ multi = ::EventMachine::MultiRequest.new
87
+ ::EventMachine.run do
88
+ queries.each_with_index do |(emitter, params), query_idx|
89
+ params ||= {}
90
+ params = params.reverse_merge(:key => key) if key
91
+ multi.add query_idx, ::EventMachine::HttpRequest.new(DOMAIN).post(:path => "/#{emitter.underscore.pluralize}.json", :body => params)
92
+ end
93
+ multi.callback do
94
+ multi.responses[:callback].each do |query_idx, http|
95
+ response = ::Hashie::Mash.new
96
+ response.status = http.response_header.status
97
+ if (200..299).include?(response.status)
98
+ response.success = true
99
+ response.merge! ::MultiJson.decode(http.response)
100
+ else
101
+ response.success = false
102
+ response.errors = [http.response]
103
+ end
104
+ unsorted[query_idx] = response
105
+ end
106
+ multi.responses[:errback].each do |query_idx, http|
107
+ response = ::Hashie::Mash.new
108
+ response.status = http.response_header.status
109
+ response.success = false
110
+ response.errors = ['Timeout or other network error.']
111
+ unsorted[query_idx] = response
112
+ end
113
+ ::EventMachine.stop
114
+ end
115
+ end
116
+ unsorted.sort_by do |query_idx, _|
117
+ query_idx
118
+ end.map do |_, response|
119
+ response
120
+ end
121
+ end
122
+
123
+ # Called when you +include Carbon+ and adds the class method +emit_as+.
124
+ # @private
125
+ def self.included(klass)
126
+ klass.extend ClassMethods
127
+ end
128
+
129
+ # Mixed into any class that includes +Carbon+.
72
130
  module ClassMethods
73
- # Indicate that this class "emits as" an <tt>:automobile</tt>, <tt>:flight</tt>, or another of the Brighter Planet emitter classes.
131
+ # DSL for declaring how to represent this class an an emitter.
74
132
  #
75
- # See the {emission estimate web service use documentation}[http://carbon.brighterplanet.com/use]
133
+ # You get this when you +include Carbon+ in a class.
76
134
  #
77
- # For example,
78
- # emit_as :automobile do
79
- # provide :make
135
+ # @param [String] emitter The {http://impact.brighterplanet.com/emitters.json camelcased emitter name}.
136
+ #
137
+ # @return [nil]
138
+ #
139
+ # Things to note in the MyFlight example:
140
+ #
141
+ # * Sending +:origin+ to Brighter Planet *as* +:origin_airport+. Otherwise Brighter Planet won't recognize +:origin+.
142
+ # * Saying we're *keying* on one code or another. Otherwise Brighter Planet will first try against full names and possibly other columns.
143
+ # * Giving *blocks* to pull codes from +MyAircraft+ and +MyAirline+ objects. Otherwise you might get a querystring like +airline[iata_code]=#<MyAirline [...]>+
144
+ #
145
+ # @example MyFlight
146
+ # # A a flight in your data warehouse
147
+ # class MyFlight
148
+ # def airline
149
+ # # ... => MyAirline(:name, :icao_code, ...)
150
+ # end
151
+ # def aircraft
152
+ # # ... => MyAircraft(:name, :icao_code, ...)
153
+ # end
154
+ # def origin
155
+ # # ... => String
156
+ # end
157
+ # def destination
158
+ # # ... => String
159
+ # end
160
+ # def segments_per_trip
161
+ # # ... => Integer
162
+ # end
163
+ # def trips
164
+ # # ... => Integer
165
+ # end
166
+ # include Carbon
167
+ # emit_as 'Flight' do
168
+ # provide :segments_per_trip
169
+ # provide :trips
170
+ # provide :origin, :as => :origin_airport, :key => :iata_code
171
+ # provide :destination, :as => :destination_airport, :key => :iata_code
172
+ # provide(:airline, :key => :iata_code) { |f| f.airline.iata_code }
173
+ # provide(:aircraft, :key => :icao_code) { { |f| f.aircraft.icao_code }
174
+ # end
80
175
  # end
81
- def emit_as(emitter_common_name, &block)
82
- Registry.instance[name] = ::Carbon::Base.new emitter_common_name
83
- ::Blockenspiel.invoke block, carbon_base
176
+ def emit_as(emitter, &blk)
177
+ emitter = emitter.to_s.singularize.camelcase
178
+ registrar = Registry::Registrar.new self, emitter
179
+ registrar.instance_eval(&blk)
84
180
  end
85
- # Third-person singular preferred.
86
- alias :emits_as :emit_as
87
-
88
- def carbon_base # :nodoc:
89
- Registry.instance[name]
181
+ end
182
+
183
+ # What will be sent to Brighter Planet CM1.
184
+ # @private
185
+ def impact_params
186
+ return unless registration = Registry.instance[self.class.name]
187
+ registration.characteristics.inject({}) do |memo, (method_id, translation_options)|
188
+ k = translation_options.has_key?(:as) ? translation_options[:as] : method_id
189
+ if translation_options.has_key?(:key)
190
+ k = "#{k}[#{translation_options[:key]}]"
191
+ end
192
+ v = if translation_options.has_key?(:blk)
193
+ translation_options[:blk].call self
194
+ else
195
+ send method_id
196
+ end
197
+ memo[k] = v
198
+ memo
90
199
  end
91
200
  end
92
201
 
93
- # Returns an emission estimate.
94
- #
95
- # Note: please see the README about <b>exceptions that you should watch out for</b>.
96
- #
97
- # You can use it like a number...
98
- # > my_car.emission_estimate + 5.1
99
- # => 415.39
100
- # Or you can get information about the response
101
- # > my_car.emission_estimate.methodology
102
- # => 'http://carbon.brighterplanet.com/automobiles.html?[...]'
103
- #
104
- # === Options:
105
- #
106
- # * <tt>:timeframe</tt> (optional) pass an instance of Timeframe[http://github.com/rossmeissl/timeframe] to request an emission for a specific time period.
107
- # * <tt>:callback</tt> (optional) where to POST the result when it's been calculated. You need a server waiting for it!
108
- # * <tt>:callback_content_type</tt> (optional if <tt>:callback</tt> is specified, ignored otherwise) pass a MIME type like 'text/yaml' so we know how to format the result when we send it to your waiting server. Defaults to 'application/json'.
109
- # * <tt>:key</tt> (optional, overrides general <tt>Carbon</tt>.<tt>key</tt> setting just for this query) If you want to use different API keys for different queries.
110
- def emission_estimate(options = {})
111
- @emission_estimate ||= ::Carbon::EmissionEstimate.new self
112
- @emission_estimate.take_options options
113
- @emission_estimate
202
+ # Get an impact estimate from Brighter Planet CM1.
203
+ #
204
+ # You get this when you +include Carbon+ in a class.
205
+ #
206
+ # The return value is a {http://rdoc.info/github/intridea/hashie/Hashie/Mash Hashie::Mash} because it's a simple way to access a deep response object.
207
+ #
208
+ # Here's a map of what's included in a response:
209
+ #
210
+ # certification
211
+ # characteristics.{}.description
212
+ # characteristics.{}.object
213
+ # compliance.[]
214
+ # decisions.{}.description
215
+ # decisions.{}.methodology
216
+ # decisions.{}.object
217
+ # emitter
218
+ # equivalents.{}
219
+ # errors.[]
220
+ # methodology
221
+ # scope
222
+ # timeframe.endDate
223
+ # timeframe.startDate
224
+ #
225
+ # @param [Hash] extra_params Anything that your +emit_as+ won't include.
226
+ #
227
+ # @option extra_params [Timeframe] :timeframe
228
+ # @option extra_params [Array<Symbol>] :comply
229
+ # @option extra_params [String] :key In case you didn't define it globally, or want to use a different one here.
230
+ #
231
+ # @return [Hashie::Mash]
232
+ #
233
+ # @example Getting impact estimate for MyFlight
234
+ # ?> my_flight = MyFlight.new([...])
235
+ # => #<MyFlight [...]>
236
+ # ?> my_impact = my_flight.impact(:timeframe => Timeframe.new(:year => 2009))
237
+ # => #<Hashie::Mash [...]>
238
+ # ?> my_impact.decisions.carbon.object.value
239
+ # => 1014.92
240
+ # ?> my_impact.decisions.carbon.object.units
241
+ # => "kilograms"
242
+ # ?> my_impact.methodology
243
+ # => "http://impact.brighterplanet.com/flights?[...]"
244
+ #
245
+ # @example How do I use a Hashie::Mash?
246
+ # ?> mash['hello']
247
+ # => "world"
248
+ # ?> mash.hello
249
+ # => "world"
250
+ # ?> mash.keys
251
+ # => ["hello"]
252
+ #
253
+ # @example Other examples of what's in the response
254
+ # my_impact.carbon.object.value
255
+ # my_impact.characteristics.airline.description
256
+ # my_impact.equivalents.lightbulbs_for_a_week
257
+ def impact(extra_params = {})
258
+ return unless registration = Registry.instance[self.class.name]
259
+ Carbon.query registration.emitter, impact_params.merge(extra_params)
114
260
  end
115
261
  end
@@ -1,6 +1,56 @@
1
1
  require 'singleton'
2
+
2
3
  module Carbon
4
+ # Used internally to hold the information about how each class that has called `emit_as`.
5
+ # @private
3
6
  class Registry < ::Hash
4
7
  include ::Singleton
8
+
9
+ # Used internally to record the emitter and parameters (characteristics) provided by a class that has called `emit_as`.
10
+ # @private
11
+ class Registration < ::Struct.new(:emitter, :characteristics)
12
+ end
13
+
14
+ # Used internally when instance-eval'ing the `emit_as` DSL.
15
+ # @private
16
+ class Registrar
17
+ # @private
18
+ def initialize(klass, emitter)
19
+ @klass = klass
20
+ Registry.instance[klass.name] = Registration.new
21
+ Registry.instance[klass.name].emitter = emitter
22
+ Registry.instance[klass.name].characteristics = {}
23
+ end
24
+
25
+ # Indicate that you will send in a piece of data about the emitter.
26
+ #
27
+ # @param [Symbol] method_id What method to call to get the value in question.
28
+ #
29
+ # @option translation_options [Symbol] :as (name of the method) If your method name does not match the Brighter Planet characteristic name.
30
+ # @option translation_options [Symbol] :key (a number of columns) What you are keying on. By default, we do a fuzzy match against a number of fields, including full names and various codes.
31
+ #
32
+ # @yield [] Pass a block for the common use case of calling a method on a object.
33
+ #
34
+ # @example Your method is named one thing but should be sent +:as+ something else.
35
+ # provide :my_distance, :as => :distance
36
+ #
37
+ # @example You are keying on something well-known like {http://en.wikipedia.org/wiki/Airline_codes IATA airline codes}.
38
+ # provide(:airline, :key => :iata_code) { |f| f.airline.iata_code }
39
+ #
40
+ # @example Better to use a block
41
+ # provide(:airline, :key => :iata_code) { |f| f.airline.iata_code }
42
+ # # is equivalent to
43
+ # def airline_iata_code
44
+ # airline.iata_code
45
+ # end
46
+ # provide :airline_iata_code, :as => :airline, :key => :iata_code
47
+ def provide(method_id, translation_options = {}, &blk)
48
+ translation_options = translation_options.dup
49
+ if block_given?
50
+ translation_options[:blk] = blk
51
+ end
52
+ Registry.instance[@klass.name].characteristics[method_id] = translation_options
53
+ end
54
+ end
5
55
  end
6
56
  end
data/lib/carbon/shell.rb CHANGED
@@ -1,8 +1,18 @@
1
- require 'brighter_planet_metadata'
1
+ require 'carbon'
2
2
  require 'bombshell'
3
3
  require 'conversions'
4
+ require 'brighter_planet_metadata'
5
+
4
6
  module Carbon
7
+ # @private
5
8
  class Shell < Bombshell::Environment
9
+ class << self
10
+ # @private
11
+ def emitters
12
+ ::BrighterPlanet.metadata.emitters
13
+ end
14
+ end
15
+
6
16
  include Bombshell::Shell
7
17
 
8
18
  before_launch do
@@ -20,24 +30,21 @@ module Carbon
20
30
 
21
31
  prompt_with 'carbon-'
22
32
 
33
+ # @private
23
34
  def help
24
35
  puts " => #{self.class.emitters.join ', '}"
25
36
  end
26
37
 
38
+ # @private
27
39
  def key(k)
28
40
  ::Carbon.key = k
29
41
  puts " => Using key #{::Carbon.key}"
30
42
  end
31
43
 
44
+ # @private
32
45
  def emitter(e, saved = {})
33
46
  Emitter.launch e, saved
34
47
  end
35
-
36
- class << self
37
- def emitters
38
- ::BrighterPlanet.metadata.emitters
39
- end
40
- end
41
48
  end
42
49
  end
43
50
 
@@ -48,4 +55,3 @@ if File.exist?(dotfile = File.join(ENV['HOME'], '.brighter_planet'))
48
55
  end
49
56
 
50
57
  require 'carbon/shell/emitter'
51
-