squake 0.1.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.
- checksums.yaml +7 -0
- data/README.md +100 -0
- data/lib/squake/api_error.rb +17 -0
- data/lib/squake/calculation_with_pricing.rb +40 -0
- data/lib/squake/client.rb +132 -0
- data/lib/squake/config.rb +24 -0
- data/lib/squake/model/items/base_type.rb +13 -0
- data/lib/squake/model/items/private_jet/squake.rb +21 -0
- data/lib/squake/model/price.rb +31 -0
- data/lib/squake/model/pricing.rb +61 -0
- data/lib/squake/model/product.rb +23 -0
- data/lib/squake/model/purchase.rb +39 -0
- data/lib/squake/products.rb +36 -0
- data/lib/squake/purchase.rb +80 -0
- data/lib/squake/response.rb +45 -0
- data/lib/squake/util.rb +29 -0
- data/lib/squake/version.rb +6 -0
- data/lib/squake.rb +7 -0
- metadata +145 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 7860b4b9cfbf3110355582ab95ce9faee0055fc6ea852b00093703fbcf8f7c8d
|
4
|
+
data.tar.gz: 6f05154a58469f7f52f9730bf808a3005961b7ba29e7f6ce8fe47e47b27cc651
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: d2065bbbe6bbbffa5067acf0810dbfe2c3ef32b50b5bce14b949f170a138f29ec16d2897093d9305af615e768ebcaa6da24873660572c24141e09f11b896cf08
|
7
|
+
data.tar.gz: 53f6a57201c32df605f6d30932baa4408bc3eafd7c02fb6f2dff6f55d0313c1e0b0f501980aa93e17bcea5ecd3bc11491f81639d138aa4c60005e62237689e92
|
data/README.md
ADDED
@@ -0,0 +1,100 @@
|
|
1
|
+
# SQUAKE
|
2
|
+
|
3
|
+
Find the documentation here: [docs](https://docs.squake.earth/).
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Via git:
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
# Gemfile
|
11
|
+
|
12
|
+
gem 'squake', git: 'git@github.com:squake-earth/squake-ruby'
|
13
|
+
```
|
14
|
+
|
15
|
+
Via rubygems:
|
16
|
+
|
17
|
+
```ruby
|
18
|
+
# Gemfile
|
19
|
+
|
20
|
+
gem 'squake', '~> 0.1.0'
|
21
|
+
```
|
22
|
+
|
23
|
+
## Auth
|
24
|
+
|
25
|
+
An API Key can be generated on the [dashboard](https://dashboard.squake.earth/.)
|
26
|
+
You need a different API Key for production and sandbox.
|
27
|
+
|
28
|
+
## Functionality
|
29
|
+
|
30
|
+
* compute emission values
|
31
|
+
* request a price quote (with a product id)
|
32
|
+
* place a purchase order (with a price quote id)
|
33
|
+
* get a list of all available products (includes price information)
|
34
|
+
|
35
|
+
## Usage
|
36
|
+
|
37
|
+
Calculate emissions and include a price quote:
|
38
|
+
|
39
|
+
```ruby
|
40
|
+
config = Squake::Config.new(
|
41
|
+
api_key: 'Your API Key', # required
|
42
|
+
sandbox_mode: true, # optional, default: true
|
43
|
+
keep_alive_timeout: 30, # optional, default: 30
|
44
|
+
logger: Logger.new($stdout), # optional, default: Logger.new($stdout)
|
45
|
+
enforced_api_base: nil, # optional, default: nil. If given, overrides the API base URL.
|
46
|
+
)
|
47
|
+
|
48
|
+
# the SQUAKE client emits canonical log lines, e.g.
|
49
|
+
# Request started http_method=get http_path=/api/v2/calculations
|
50
|
+
client = Squake::Client.new(config: config)
|
51
|
+
|
52
|
+
# Find all available item types and methodologies here:
|
53
|
+
# https://docs-v2.squake.earth/group/endpoint-calculations
|
54
|
+
items = [
|
55
|
+
{
|
56
|
+
type: 'flight',
|
57
|
+
methodology: 'ICAO',
|
58
|
+
origin: 'BER',
|
59
|
+
destination: 'SIN',
|
60
|
+
booking_class: 'economy',
|
61
|
+
number_of_travellers: 2,
|
62
|
+
aircraft_type: '350',
|
63
|
+
external_reference: 'booking-id',
|
64
|
+
}
|
65
|
+
]
|
66
|
+
|
67
|
+
# returns Squake::Model::Pricing
|
68
|
+
pricing = Squake::CalculationWithPricing.quote(
|
69
|
+
client: client, # required
|
70
|
+
items: items, # required
|
71
|
+
product: 'product-id', # required
|
72
|
+
currency: 'EUR', # optional, default: 'EUR'
|
73
|
+
carbon_unit: 'gram', # optional, default: 'gram', other options: 'kilogram', 'tonne'
|
74
|
+
)
|
75
|
+
```
|
76
|
+
|
77
|
+
Place a purchase order; by default the library injects a `SecureRandom.uuid` as `external_reference` to ensure idempotency, i.e. you can safely retry the request if it fails.
|
78
|
+
|
79
|
+
```ruby
|
80
|
+
uuid = SecureRandom.uuid
|
81
|
+
|
82
|
+
# returns Squake::Model::Purchase
|
83
|
+
purchase = Squake::Purchase.create(
|
84
|
+
client: client, # required
|
85
|
+
pricing: pricing.id, # required, from above
|
86
|
+
external_reference: uuid, # optional, default: SecureRandom.uuid, used for idempotency, if given, MUST be unique
|
87
|
+
)
|
88
|
+
|
89
|
+
# retrieve the purchase later
|
90
|
+
Squake::Purchase.retrieve(
|
91
|
+
client: client, # required
|
92
|
+
id: purchase.id, # required
|
93
|
+
)
|
94
|
+
|
95
|
+
# within 14 days, you can cancel the purchase worry-free
|
96
|
+
Squake::Purchase.cancel(
|
97
|
+
client: client, # required
|
98
|
+
id: purchase.id, # required
|
99
|
+
)
|
100
|
+
```
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Squake
|
5
|
+
class APIError < StandardError
|
6
|
+
extend T::Sig
|
7
|
+
|
8
|
+
sig { returns(Squake::Response) }
|
9
|
+
attr_reader :response
|
10
|
+
|
11
|
+
sig { params(response: Squake::Response).void }
|
12
|
+
def initialize(response:)
|
13
|
+
@response = response
|
14
|
+
super(T.must(response.error_message))
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
# https://docs-v2.squake.earth/group/endpoint-combined-calculation-pricing
|
5
|
+
module Squake
|
6
|
+
class CalculationWithPricing
|
7
|
+
extend T::Sig
|
8
|
+
|
9
|
+
ENDPOINT = T.let('/v2/calculations-with-pricing', String)
|
10
|
+
|
11
|
+
sig do
|
12
|
+
params(
|
13
|
+
client: Squake::Client,
|
14
|
+
items: T::Array[Squake::Model::Items::BaseType],
|
15
|
+
product: String,
|
16
|
+
currency: String,
|
17
|
+
carbon_unit: String,
|
18
|
+
expand: T::Array[String],
|
19
|
+
).returns(Squake::Model::Pricing)
|
20
|
+
end
|
21
|
+
def self.quote(client:, items:, product:, currency: 'EUR', carbon_unit: 'gram', expand: [])
|
22
|
+
result = client.call(
|
23
|
+
path: ENDPOINT,
|
24
|
+
method: :post,
|
25
|
+
params: {
|
26
|
+
items: items.map(&:serialize),
|
27
|
+
product: product,
|
28
|
+
currency: currency,
|
29
|
+
carbon_unit: carbon_unit,
|
30
|
+
expand: expand,
|
31
|
+
},
|
32
|
+
)
|
33
|
+
raise Squake::APIError.new(response: result) unless result.success?
|
34
|
+
|
35
|
+
Squake::Model::Pricing.from_api_response(
|
36
|
+
T.cast(result.body, T::Hash[Symbol, T.untyped]),
|
37
|
+
)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,132 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'oj'
|
5
|
+
|
6
|
+
module Squake
|
7
|
+
class Client
|
8
|
+
extend T::Sig
|
9
|
+
|
10
|
+
JsonResponseBody = T.type_alias do
|
11
|
+
T.any(
|
12
|
+
T::Array[T::Hash[Symbol, T.untyped]],
|
13
|
+
T::Hash[Symbol, T.untyped],
|
14
|
+
)
|
15
|
+
end
|
16
|
+
|
17
|
+
sig { params(config: Squake::Config).void }
|
18
|
+
def initialize(config:)
|
19
|
+
@config = config
|
20
|
+
end
|
21
|
+
|
22
|
+
sig do
|
23
|
+
params(
|
24
|
+
path: String,
|
25
|
+
headers: T::Hash[String, String],
|
26
|
+
method: Symbol,
|
27
|
+
params: T.nilable(T::Hash[Symbol, String]),
|
28
|
+
api_base: T.nilable(String),
|
29
|
+
api_key: T.nilable(String),
|
30
|
+
).returns(Squake::Response)
|
31
|
+
end
|
32
|
+
def call(path:, headers: {}, method: :get, params: nil, api_base: nil, api_key: nil)
|
33
|
+
execute_request(
|
34
|
+
method: method,
|
35
|
+
path: path,
|
36
|
+
headers: headers,
|
37
|
+
params: params,
|
38
|
+
api_base: api_base,
|
39
|
+
api_key: api_key,
|
40
|
+
)
|
41
|
+
end
|
42
|
+
|
43
|
+
# rubocop:disable Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity
|
44
|
+
sig do
|
45
|
+
params(
|
46
|
+
method: Symbol,
|
47
|
+
path: String,
|
48
|
+
headers: T::Hash[String, String],
|
49
|
+
params: T.nilable(T::Hash[Symbol, String]),
|
50
|
+
api_base: T.nilable(String),
|
51
|
+
api_key: T.nilable(String),
|
52
|
+
).returns(Squake::Response)
|
53
|
+
end
|
54
|
+
private def execute_request(method:, path:, headers: {}, params: nil, api_base: nil, api_key: nil)
|
55
|
+
api_base ||= @config.api_base
|
56
|
+
api_key ||= @config.api_key
|
57
|
+
|
58
|
+
body_params = nil
|
59
|
+
query_params = nil
|
60
|
+
case method
|
61
|
+
when :get, :head, :delete
|
62
|
+
query_params = params
|
63
|
+
when :post
|
64
|
+
body_params = params
|
65
|
+
else
|
66
|
+
raise "Unrecognized HTTP method: #{method}. Expected one of: get, head, delete, post"
|
67
|
+
end
|
68
|
+
|
69
|
+
headers = request_headers(api_key).update(headers)
|
70
|
+
query = query_params ? Util.encode_parameters(query_params) : nil
|
71
|
+
body = body_params ? ::Oj.dump(body_params) : nil
|
72
|
+
sanitized_path = path[0] == '/' ? path : "/#{path}"
|
73
|
+
uri = URI.parse(api_base + sanitized_path)
|
74
|
+
|
75
|
+
connection = connection(uri)
|
76
|
+
|
77
|
+
method_name = method.to_s.upcase
|
78
|
+
has_response_body = method_name != 'HEAD'
|
79
|
+
request = Net::HTTPGenericRequest.new(
|
80
|
+
method_name,
|
81
|
+
(body_params ? true : false),
|
82
|
+
has_response_body,
|
83
|
+
(query ? [uri.path, query].join('?') : uri.path),
|
84
|
+
headers,
|
85
|
+
)
|
86
|
+
|
87
|
+
# https://stripe.com/blog/canonical-log-lines
|
88
|
+
canonical_request_line = <<~TXT
|
89
|
+
Request started
|
90
|
+
http_method=#{request.method}
|
91
|
+
http_path=#{request.path}
|
92
|
+
http_headers=#{request.to_hash}
|
93
|
+
TXT
|
94
|
+
canonical_request_line.gsub!(api_key, 'API_KEY_REDACTED')
|
95
|
+
@config.logger.info(canonical_request_line)
|
96
|
+
|
97
|
+
result = connection.request(request, body)
|
98
|
+
|
99
|
+
Squake::Response.new(
|
100
|
+
original_request: request,
|
101
|
+
code: Integer(result.code),
|
102
|
+
body: try_parse_json(result.body),
|
103
|
+
headers: result.to_hash,
|
104
|
+
)
|
105
|
+
end
|
106
|
+
# rubocop:enable Metrics/MethodLength, Metrics/AbcSize, Metrics/CyclomaticComplexity
|
107
|
+
|
108
|
+
sig { params(api_key: String).returns(T::Hash[String, String]) }
|
109
|
+
private def request_headers(api_key)
|
110
|
+
{
|
111
|
+
'Authorization' => "Bearer #{api_key}",
|
112
|
+
'Content-Type' => 'application/json',
|
113
|
+
}
|
114
|
+
end
|
115
|
+
|
116
|
+
sig { params(result_body: T.untyped).returns(JsonResponseBody) }
|
117
|
+
private def try_parse_json(result_body)
|
118
|
+
::Oj.load(result_body, symbol_keys: true)
|
119
|
+
rescue ::Oj::ParseError, TypeError, JSON::ParserError, EncodingError => e
|
120
|
+
# in case of an error, Squake's response body is HTML not JSON
|
121
|
+
{ 'error' => { 'message' => e.message, 'body' => result_body } }
|
122
|
+
end
|
123
|
+
|
124
|
+
sig { params(uri: URI::Generic).returns(Net::HTTP) }
|
125
|
+
private def connection(uri)
|
126
|
+
connection = Net::HTTP.new(uri.host, uri.port)
|
127
|
+
connection.keep_alive_timeout = @config.keep_alive_timeout
|
128
|
+
connection.use_ssl = uri.scheme == 'https'
|
129
|
+
connection
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Squake
|
5
|
+
class Config < T::Struct
|
6
|
+
extend T::Sig
|
7
|
+
|
8
|
+
DEFAULT_BASE_URL = T.let('https://api.squake.earth', String)
|
9
|
+
SANDBOX_BASE_URL = T.let('https://api.sandbox.squake.earth', String)
|
10
|
+
|
11
|
+
const :api_key, String
|
12
|
+
const :keep_alive_timeout, Integer, default: 30
|
13
|
+
const :logger, Logger, factory: -> { Logger.new($stdout) }
|
14
|
+
const :sandbox_mode, T::Boolean, default: true
|
15
|
+
const :enforced_api_base, T.nilable(String)
|
16
|
+
|
17
|
+
sig { returns(String) }
|
18
|
+
def api_base
|
19
|
+
return T.must(enforced_api_base) unless enforced_api_base.nil?
|
20
|
+
|
21
|
+
sandbox_mode ? SANDBOX_BASE_URL : DEFAULT_BASE_URL
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# typed: strict
|
2
|
+
|
3
|
+
module Squake
|
4
|
+
module Model
|
5
|
+
module Items
|
6
|
+
module PrivateJet
|
7
|
+
class Squake < T::Struct
|
8
|
+
extend T::Sig
|
9
|
+
|
10
|
+
const :type, String, default: 'private_jet'
|
11
|
+
const :methodology, String, default: 'SQUAKE'
|
12
|
+
const :origin, String
|
13
|
+
const :destination, String
|
14
|
+
const :identifier, String
|
15
|
+
const :identifier_kind, String, default: 'ICAO'
|
16
|
+
const :external_reference, T.nilable(String)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Squake
|
5
|
+
module Model
|
6
|
+
class Price < T::Struct
|
7
|
+
extend T::Sig
|
8
|
+
|
9
|
+
const :id, String
|
10
|
+
const :product, String
|
11
|
+
const :unit_amount, BigDecimal
|
12
|
+
const :valid_from, T.nilable(Date)
|
13
|
+
const :carbon_unit, String
|
14
|
+
const :currency, String
|
15
|
+
|
16
|
+
sig { params(response_body: T::Hash[Symbol, T.untyped]).returns(Squake::Model::Price) }
|
17
|
+
def self.from_api_response(response_body)
|
18
|
+
product = response_body.fetch(:price, {})
|
19
|
+
|
20
|
+
Squake::Model::Price.new(
|
21
|
+
id: product.fetch(:id),
|
22
|
+
product: product.fetch(:product),
|
23
|
+
unit_amount: product.fetch(:unit_amount).to_d,
|
24
|
+
valid_from: Date.parse(product.fetch(:valid_from, nil)),
|
25
|
+
carbon_unit: product.fetch(:carbon_unit),
|
26
|
+
currency: product.fetch(:currency),
|
27
|
+
)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require_relative 'price'
|
5
|
+
require_relative 'product'
|
6
|
+
|
7
|
+
module Squake
|
8
|
+
module Model
|
9
|
+
class Pricing < T::Struct
|
10
|
+
extend T::Sig
|
11
|
+
|
12
|
+
class Item < T::Struct
|
13
|
+
extend T::Sig
|
14
|
+
const :carbon_quantity, BigDecimal
|
15
|
+
const :carbon_unit, String
|
16
|
+
const :distance, BigDecimal
|
17
|
+
const :distance_unit, String
|
18
|
+
const :external_reference, T.nilable(String)
|
19
|
+
const :type, String
|
20
|
+
const :methodology, String
|
21
|
+
end
|
22
|
+
|
23
|
+
const :id, String
|
24
|
+
const :carbon_quantity, BigDecimal
|
25
|
+
const :carbon_unit, String
|
26
|
+
const :payment_link, T.nilable(String)
|
27
|
+
const :price, Squake::Model::Price
|
28
|
+
const :product, Squake::Model::Product
|
29
|
+
const :valid_until, Date
|
30
|
+
const :currency, String, default: 'EUR'
|
31
|
+
const :total, Integer
|
32
|
+
const :items, T::Array[Pricing::Item] # when constructing this, construct nested items first explicitly
|
33
|
+
|
34
|
+
sig { params(response_body: T::Hash[Symbol, T.untyped]).returns(Squake::Model::Pricing) }
|
35
|
+
def self.from_api_response(response_body)
|
36
|
+
price = Squake::Model::Price.from_api_response(response_body)
|
37
|
+
product = Squake::Model::Product.from_api_response(response_body)
|
38
|
+
|
39
|
+
items = response_body.fetch(:items, [])
|
40
|
+
items.map! do |item|
|
41
|
+
item[:carbon_quantity] = item.fetch(:carbon_quantity).to_d
|
42
|
+
item[:distance] = item.fetch(:distance).to_d
|
43
|
+
Item.new(item)
|
44
|
+
end
|
45
|
+
|
46
|
+
Squake::Model::Pricing.new(
|
47
|
+
id: response_body.fetch(:id),
|
48
|
+
items: items,
|
49
|
+
carbon_quantity: response_body.fetch(:carbon_quantity).to_d,
|
50
|
+
carbon_unit: response_body.fetch(:carbon_unit),
|
51
|
+
payment_link: response_body.fetch(:payment_link, nil),
|
52
|
+
price: price,
|
53
|
+
product: product,
|
54
|
+
valid_until: Date.parse(response_body.fetch(:valid_until, nil)),
|
55
|
+
currency: response_body.fetch(:currency),
|
56
|
+
total: response_body.fetch(:total),
|
57
|
+
)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Squake
|
5
|
+
module Model
|
6
|
+
class Product < T::Struct
|
7
|
+
extend T::Sig
|
8
|
+
|
9
|
+
const :id, String
|
10
|
+
const :title, String
|
11
|
+
|
12
|
+
sig { params(response_body: T::Hash[Symbol, T.untyped]).returns(Squake::Model::Product) }
|
13
|
+
def self.from_api_response(response_body)
|
14
|
+
product = response_body.fetch(:product, {})
|
15
|
+
|
16
|
+
Squake::Model::Product.new(
|
17
|
+
id: product.fetch(:id),
|
18
|
+
title: product.fetch(:title),
|
19
|
+
)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Squake
|
5
|
+
module Model
|
6
|
+
class Purchase < T::Struct
|
7
|
+
extend T::Sig
|
8
|
+
|
9
|
+
const :id, String
|
10
|
+
const :files, T::Array[String] # TODO: optional object, typed?
|
11
|
+
const :payment_method, String
|
12
|
+
const :state, String
|
13
|
+
const :metadata, T::Hash[Symbol, T.untyped], default: {}
|
14
|
+
const :checkout_page, T.nilable(T::Hash[Symbol, T.untyped])
|
15
|
+
const :carbon_quantity, BigDecimal
|
16
|
+
const :carbon_unit, String
|
17
|
+
const :total, Integer
|
18
|
+
const :currency, String
|
19
|
+
const :external_reference, T.nilable(String)
|
20
|
+
|
21
|
+
sig { params(response_body: T::Hash[Symbol, T.untyped]).returns(Squake::Model::Purchase) }
|
22
|
+
def self.from_api_response(response_body)
|
23
|
+
Squake::Model::Purchase.new(
|
24
|
+
id: response_body.fetch(:id),
|
25
|
+
files: response_body.fetch(:files),
|
26
|
+
payment_method: response_body.fetch(:payment_method),
|
27
|
+
state: response_body.fetch(:state),
|
28
|
+
metadata: response_body.fetch(:metadata),
|
29
|
+
checkout_page: response_body.fetch(:checkout_page, nil),
|
30
|
+
carbon_quantity: BigDecimal(response_body.fetch(:carbon_quantity).to_s),
|
31
|
+
carbon_unit: response_body.fetch(:carbon_unit),
|
32
|
+
total: response_body.fetch(:total),
|
33
|
+
currency: response_body.fetch(:currency),
|
34
|
+
external_reference: response_body.fetch(:external_reference, nil),
|
35
|
+
)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
# https://docs-v2.squake.earth/operation/operation-get-products
|
5
|
+
module Squake
|
6
|
+
class Products
|
7
|
+
extend T::Sig
|
8
|
+
|
9
|
+
ENDPOINT = T.let('/v2/products', String)
|
10
|
+
DEFAULT_LOCALE = T.let('en', String)
|
11
|
+
|
12
|
+
sig do
|
13
|
+
params(
|
14
|
+
client: Squake::Client,
|
15
|
+
locale: String,
|
16
|
+
product_id: T.nilable(String),
|
17
|
+
).returns(T::Array[Squake::Model::Product])
|
18
|
+
end
|
19
|
+
def self.get(client:, locale: DEFAULT_LOCALE, product_id: nil)
|
20
|
+
path = product_id.nil? ? ENDPOINT : "#{ENDPOINT}/#{product_id}"
|
21
|
+
|
22
|
+
result = client.call(
|
23
|
+
path: path,
|
24
|
+
method: :get,
|
25
|
+
params: {
|
26
|
+
locale: locale,
|
27
|
+
},
|
28
|
+
)
|
29
|
+
raise Squake::APIError.new(response: result) unless result.success?
|
30
|
+
|
31
|
+
Array(result.body).map do |product_data|
|
32
|
+
Squake::Model::Product.from_api_response({ product: product_data })
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
# https://docs-v2.squake.earth/operation/operation-post-purchases
|
5
|
+
module Squake
|
6
|
+
class Purchase
|
7
|
+
extend T::Sig
|
8
|
+
|
9
|
+
ENDPOINT = T.let('/v2/purchases', String)
|
10
|
+
|
11
|
+
# rubocop:disable Metrics/ParameterLists, Layout/LineLength
|
12
|
+
sig do
|
13
|
+
params(
|
14
|
+
client: Squake::Client,
|
15
|
+
pricing: String,
|
16
|
+
confirmation_document: T.nilable(T::Hash[Symbol, T.untyped]),
|
17
|
+
certificate_document: T.nilable(T::Hash[Symbol, T.untyped]),
|
18
|
+
metadata: T.nilable(T::Hash[Symbol, T.untyped]),
|
19
|
+
external_reference: String, # used for idempotency, if given, MUST be unique
|
20
|
+
expand: T::Array[String],
|
21
|
+
).returns(Squake::Model::Purchase)
|
22
|
+
end
|
23
|
+
def self.create(client:, pricing:, confirmation_document: nil, certificate_document: nil, metadata: nil, external_reference: SecureRandom.uuid, expand: [])
|
24
|
+
result = client.call(
|
25
|
+
path: ENDPOINT,
|
26
|
+
method: :post,
|
27
|
+
params: {
|
28
|
+
pricing: pricing,
|
29
|
+
confirmation_document: confirmation_document,
|
30
|
+
certificate_document: certificate_document,
|
31
|
+
metadata: metadata,
|
32
|
+
external_reference: external_reference,
|
33
|
+
expand: expand,
|
34
|
+
},
|
35
|
+
)
|
36
|
+
raise Squake::APIError.new(response: result) unless result.success?
|
37
|
+
|
38
|
+
Squake::Model::Purchase.from_api_response(
|
39
|
+
T.cast(result.body, T::Hash[Symbol, T.untyped]),
|
40
|
+
)
|
41
|
+
end
|
42
|
+
# rubocop:enable Metrics/ParameterLists, Layout/LineLength
|
43
|
+
|
44
|
+
sig do
|
45
|
+
params(
|
46
|
+
client: Squake::Client,
|
47
|
+
id: String,
|
48
|
+
).returns(T.nilable(Squake::Model::Purchase))
|
49
|
+
end
|
50
|
+
def self.retrieve(client:, id:)
|
51
|
+
result = client.call(
|
52
|
+
path: "#{ENDPOINT}/#{id}",
|
53
|
+
)
|
54
|
+
return nil if result.code == 404
|
55
|
+
raise Squake::APIError.new(response: result) unless result.success?
|
56
|
+
|
57
|
+
Squake::Model::Purchase.from_api_response(
|
58
|
+
T.cast(result.body, T::Hash[Symbol, T.untyped]),
|
59
|
+
)
|
60
|
+
end
|
61
|
+
|
62
|
+
sig do
|
63
|
+
params(
|
64
|
+
client: Squake::Client,
|
65
|
+
id: String,
|
66
|
+
).returns(T.nilable(Squake::Model::Purchase))
|
67
|
+
end
|
68
|
+
def self.cancel(client:, id:)
|
69
|
+
result = client.call(
|
70
|
+
path: "#{ENDPOINT}/#{id}/cancel",
|
71
|
+
method: :post,
|
72
|
+
)
|
73
|
+
raise Squake::APIError.new(response: result) unless result.success?
|
74
|
+
|
75
|
+
Squake::Model::Purchase.from_api_response(
|
76
|
+
T.cast(result.body, T::Hash[Symbol, T.untyped]),
|
77
|
+
)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'oj'
|
5
|
+
|
6
|
+
module Squake
|
7
|
+
class Response < T::Struct
|
8
|
+
extend T::Sig
|
9
|
+
|
10
|
+
const :original_request, Net::HTTPGenericRequest
|
11
|
+
const :code, Integer
|
12
|
+
const :body, Squake::Client::JsonResponseBody
|
13
|
+
const :headers, T::Hash[String, T.untyped]
|
14
|
+
|
15
|
+
sig { returns(T::Boolean) }
|
16
|
+
def success?
|
17
|
+
code >= 200 && code < 300
|
18
|
+
end
|
19
|
+
|
20
|
+
sig { returns(T::Boolean) }
|
21
|
+
def failure?
|
22
|
+
!success?
|
23
|
+
end
|
24
|
+
|
25
|
+
sig { returns(T.nilable(Squake::APIError)) }
|
26
|
+
def error
|
27
|
+
return if success?
|
28
|
+
|
29
|
+
Squake::APIError.new(response: self)
|
30
|
+
end
|
31
|
+
|
32
|
+
sig { returns(T.nilable(String)) }
|
33
|
+
def error_message
|
34
|
+
return if success?
|
35
|
+
|
36
|
+
<<~TXT
|
37
|
+
Failed Request
|
38
|
+
http_code=#{code}
|
39
|
+
body=#{::Oj.dump(body)}
|
40
|
+
original_request_http_method=#{original_request.method}
|
41
|
+
original_request_http_path=#{original_request.path}
|
42
|
+
TXT
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
data/lib/squake/util.rb
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
module Squake
|
5
|
+
class Util
|
6
|
+
extend T::Sig
|
7
|
+
|
8
|
+
# Encodes a hash of parameters in a way that's suitable for use as query
|
9
|
+
# parameters in a URI or as form parameters in a request body. This mainly
|
10
|
+
# involves escaping special characters from parameter keys and values (e.g.
|
11
|
+
# `&`).
|
12
|
+
sig { params(params: T::Hash[Symbol, String]).returns(String) }
|
13
|
+
def self.encode_parameters(params)
|
14
|
+
params.map { |k, v| "#{url_encode(k.to_s)}=#{url_encode(v)}" }.join('&')
|
15
|
+
end
|
16
|
+
|
17
|
+
# Encodes a string in a way that makes it suitable for use in a set of
|
18
|
+
# query parameters in a URI or in a set of form parameters in a request
|
19
|
+
# body.
|
20
|
+
sig { params(key: String).returns(String) }
|
21
|
+
def self.url_encode(key)
|
22
|
+
CGI.escape(key.to_s).
|
23
|
+
# Don't use strict form encoding by changing the square bracket control
|
24
|
+
# characters back to their literals. This is fine by the server, and
|
25
|
+
# makes these parameter strings easier to read.
|
26
|
+
gsub('%5B', '[').gsub('%5D', ']')
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
data/lib/squake.rb
ADDED
metadata
ADDED
@@ -0,0 +1,145 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: squake
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- 'Team SQUAKE :rocket:'
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2023-06-14 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: oj
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: sorbet
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: sorbet-runtime
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rubocop-dbl
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: spoom
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: tapioca
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
description: This gem provides an interface for the SQUAKE API to calculate and compensate
|
98
|
+
carbon emissions.
|
99
|
+
email: oss@squake.earth
|
100
|
+
executables: []
|
101
|
+
extensions: []
|
102
|
+
extra_rdoc_files: []
|
103
|
+
files:
|
104
|
+
- README.md
|
105
|
+
- lib/squake.rb
|
106
|
+
- lib/squake/api_error.rb
|
107
|
+
- lib/squake/calculation_with_pricing.rb
|
108
|
+
- lib/squake/client.rb
|
109
|
+
- lib/squake/config.rb
|
110
|
+
- lib/squake/model/items/base_type.rb
|
111
|
+
- lib/squake/model/items/private_jet/squake.rb
|
112
|
+
- lib/squake/model/price.rb
|
113
|
+
- lib/squake/model/pricing.rb
|
114
|
+
- lib/squake/model/product.rb
|
115
|
+
- lib/squake/model/purchase.rb
|
116
|
+
- lib/squake/products.rb
|
117
|
+
- lib/squake/purchase.rb
|
118
|
+
- lib/squake/response.rb
|
119
|
+
- lib/squake/util.rb
|
120
|
+
- lib/squake/version.rb
|
121
|
+
homepage: https://github.com/squake-earth/squake-ruby
|
122
|
+
licenses:
|
123
|
+
- MIT
|
124
|
+
metadata:
|
125
|
+
rubygems_mfa_required: 'true'
|
126
|
+
post_install_message:
|
127
|
+
rdoc_options: []
|
128
|
+
require_paths:
|
129
|
+
- lib
|
130
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
131
|
+
requirements:
|
132
|
+
- - ">="
|
133
|
+
- !ruby/object:Gem::Version
|
134
|
+
version: '2.7'
|
135
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
136
|
+
requirements:
|
137
|
+
- - ">="
|
138
|
+
- !ruby/object:Gem::Version
|
139
|
+
version: '0'
|
140
|
+
requirements: []
|
141
|
+
rubygems_version: 3.4.13
|
142
|
+
signing_key:
|
143
|
+
specification_version: 4
|
144
|
+
summary: The industry solution for sustainable travel and logistics.
|
145
|
+
test_files: []
|