carbon 1.1.3 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +4 -22
- data/CHANGELOG +11 -0
- data/Gemfile +10 -1
- data/README.markdown +185 -0
- data/Rakefile +13 -26
- data/bin/carbon +3 -3
- data/carbon.gemspec +17 -23
- data/developer_notes/MULTI.markdown +25 -0
- data/developer_notes/REDUCE_HTTP_CONNECTIONS.markdown +46 -0
- data/features/shell.feature +1 -1
- data/features/support/env.rb +3 -4
- data/lib/carbon.rb +242 -96
- data/lib/carbon/registry.rb +50 -0
- data/lib/carbon/shell.rb +14 -8
- data/lib/carbon/shell/emitter.rb +33 -29
- data/lib/carbon/version.rb +1 -1
- data/test/carbon_test.rb +167 -0
- metadata +128 -182
- data/MIT-LICENSE.txt +0 -19
- data/README.rdoc +0 -266
- data/doc/INTEGRATION_GUIDE.rdoc +0 -1002
- data/doc/examining-response-with-jsonview.png +0 -0
- data/doc/shell_example +0 -43
- data/doc/timeout-error.png +0 -0
- data/doc/with-committee-reports.png +0 -0
- data/doc/without-committee-reports.png +0 -0
- data/lib/carbon/base.rb +0 -62
- data/lib/carbon/emission_estimate.rb +0 -165
- data/lib/carbon/emission_estimate/request.rb +0 -100
- data/lib/carbon/emission_estimate/response.rb +0 -61
- data/lib/carbon/emission_estimate/storage.rb +0 -33
- data/spec/fixtures/vcr_cassettes/flight.yml +0 -47
- data/spec/fixtures/vcr_cassettes/residence.yml +0 -44
- data/spec/lib/carbon/emission_estimate/request_spec.rb +0 -41
- data/spec/lib/carbon/emission_estimate/response_spec.rb +0 -33
- data/spec/lib/carbon/emission_estimate_spec.rb +0 -32
- data/spec/lib/carbon_spec.rb +0 -384
- data/spec/spec_helper.rb +0 -60
- data/spec/specwatchr +0 -60
data/lib/carbon.rb
CHANGED
@@ -1,115 +1,261 @@
|
|
1
|
-
require '
|
2
|
-
require '
|
3
|
-
require '
|
4
|
-
require '
|
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 '
|
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
|
-
|
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
|
-
#
|
57
|
-
|
11
|
+
# @private
|
12
|
+
# Make sure there are no warnings about class vars.
|
13
|
+
@@key = nil unless defined?(@@key)
|
58
14
|
|
59
|
-
|
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
|
-
|
62
|
-
|
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
|
-
|
66
|
-
|
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
|
-
#
|
65
|
+
|
66
|
+
# Perform many queries in parallel. Can be >90% faster than doing them serially (one after the other).
|
70
67
|
#
|
71
|
-
#
|
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
|
-
#
|
131
|
+
# DSL for declaring how to represent this class an an emitter.
|
74
132
|
#
|
75
|
-
#
|
133
|
+
# You get this when you +include Carbon+ in a class.
|
76
134
|
#
|
77
|
-
#
|
78
|
-
#
|
79
|
-
#
|
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(
|
82
|
-
|
83
|
-
::
|
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
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
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
|
-
#
|
94
|
-
#
|
95
|
-
#
|
96
|
-
#
|
97
|
-
#
|
98
|
-
#
|
99
|
-
#
|
100
|
-
#
|
101
|
-
#
|
102
|
-
#
|
103
|
-
#
|
104
|
-
#
|
105
|
-
#
|
106
|
-
#
|
107
|
-
#
|
108
|
-
#
|
109
|
-
#
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
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
|
data/lib/carbon/registry.rb
CHANGED
@@ -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 '
|
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
|
-
|