carbon 1.1.3 → 2.0.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.
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
-