latitude-api 0.2.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/.yardopts +10 -0
- data/LICENSE.txt +21 -0
- data/README.md +283 -0
- data/Rakefile +13 -0
- data/lib/latitude/api/api_object.rb +105 -0
- data/lib/latitude/api/api_resource.rb +260 -0
- data/lib/latitude/api/attributes.rb +64 -0
- data/lib/latitude/api/client.rb +80 -0
- data/lib/latitude/api/configuration.rb +88 -0
- data/lib/latitude/api/errors.rb +134 -0
- data/lib/latitude/api/json_api.rb +40 -0
- data/lib/latitude/api/list_object.rb +129 -0
- data/lib/latitude/api/objects.rb +216 -0
- data/lib/latitude/api/operations/action.rb +41 -0
- data/lib/latitude/api/operations/create.rb +22 -0
- data/lib/latitude/api/operations/delete.rb +22 -0
- data/lib/latitude/api/operations/list.rb +54 -0
- data/lib/latitude/api/operations/nested.rb +53 -0
- data/lib/latitude/api/operations/retrieve.rb +22 -0
- data/lib/latitude/api/operations/update.rb +22 -0
- data/lib/latitude/api/request_executor.rb +217 -0
- data/lib/latitude/api/request_options.rb +86 -0
- data/lib/latitude/api/request_params.rb +70 -0
- data/lib/latitude/api/resources/api_key.rb +61 -0
- data/lib/latitude/api/resources/billing_usage.rb +51 -0
- data/lib/latitude/api/resources/elastic_ip.rb +96 -0
- data/lib/latitude/api/resources/event.rb +55 -0
- data/lib/latitude/api/resources/firewall.rb +129 -0
- data/lib/latitude/api/resources/ip.rb +110 -0
- data/lib/latitude/api/resources/kubernetes_cluster.rb +184 -0
- data/lib/latitude/api/resources/plan.rb +658 -0
- data/lib/latitude/api/resources/project.rb +179 -0
- data/lib/latitude/api/resources/region.rb +41 -0
- data/lib/latitude/api/resources/role.rb +20 -0
- data/lib/latitude/api/resources/server.rb +251 -0
- data/lib/latitude/api/resources/ssh_key.rb +44 -0
- data/lib/latitude/api/resources/storage_filesystem.rb +36 -0
- data/lib/latitude/api/resources/storage_object.rb +85 -0
- data/lib/latitude/api/resources/storage_volume.rb +64 -0
- data/lib/latitude/api/resources/tag.rb +34 -0
- data/lib/latitude/api/resources/team.rb +169 -0
- data/lib/latitude/api/resources/traffic.rb +215 -0
- data/lib/latitude/api/resources/user_data.rb +38 -0
- data/lib/latitude/api/resources/user_profile.rb +51 -0
- data/lib/latitude/api/resources/user_team.rb +90 -0
- data/lib/latitude/api/resources/virtual_machine.rb +146 -0
- data/lib/latitude/api/resources/virtual_network.rb +162 -0
- data/lib/latitude/api/resources/vpn_session.rb +80 -0
- data/lib/latitude/api/singleton_api_resource.rb +22 -0
- data/lib/latitude/api/util.rb +64 -0
- data/lib/latitude/api/version.rb +7 -0
- data/lib/latitude/api.rb +147 -0
- data/lib/latitude/metadata/errors.rb +18 -0
- data/lib/latitude/metadata/session.rb +109 -0
- data/lib/latitude/metadata.rb +43 -0
- data/lib/latitude.rb +4 -0
- metadata +132 -0
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Latitude
|
|
4
|
+
module API
|
|
5
|
+
# Shared, read-only value objects that appear as nested attributes across
|
|
6
|
+
# many resources (project/team/currency/limits/stats/billing/tag/user/role).
|
|
7
|
+
module Objects
|
|
8
|
+
# @!attribute [r] id
|
|
9
|
+
# @return [String]
|
|
10
|
+
# @!attribute [r] code
|
|
11
|
+
# @return [String]
|
|
12
|
+
# @!attribute [r] name
|
|
13
|
+
# @return [String]
|
|
14
|
+
# @!attribute [r] currency_id
|
|
15
|
+
# @return [String]
|
|
16
|
+
class Currency < APIObject
|
|
17
|
+
attribute :id, :string, read_only: true
|
|
18
|
+
attribute :code, :string, read_only: true
|
|
19
|
+
attribute :name, :string, read_only: true
|
|
20
|
+
attribute :currency_id, :string, read_only: true
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# @!attribute [r] bare_metal
|
|
24
|
+
# @return [Integer]
|
|
25
|
+
# @!attribute [r] bare_metal_gpu
|
|
26
|
+
# @return [Integer]
|
|
27
|
+
# @!attribute [r] virtual_machine
|
|
28
|
+
# @return [Integer]
|
|
29
|
+
# @!attribute [r] virtual_machine_gpu
|
|
30
|
+
# @return [Integer]
|
|
31
|
+
# @!attribute [r] elastic_ip
|
|
32
|
+
# @return [Integer]
|
|
33
|
+
# @!attribute [r] virtual_network
|
|
34
|
+
# @return [Integer]
|
|
35
|
+
# @!attribute [r] database
|
|
36
|
+
# @return [String]
|
|
37
|
+
# @!attribute [r] filesystem
|
|
38
|
+
# @return [String]
|
|
39
|
+
# @!attribute [r] block_storage
|
|
40
|
+
# @return [String]
|
|
41
|
+
class Limits < APIObject
|
|
42
|
+
attribute :bare_metal, :integer, read_only: true
|
|
43
|
+
attribute :bare_metal_gpu, :integer, read_only: true
|
|
44
|
+
attribute :virtual_machine, :integer, read_only: true
|
|
45
|
+
attribute :virtual_machine_gpu, :integer, read_only: true
|
|
46
|
+
attribute :elastic_ip, :integer, read_only: true
|
|
47
|
+
attribute :virtual_network, :integer, read_only: true
|
|
48
|
+
attribute :database, :string, read_only: true
|
|
49
|
+
attribute :filesystem, :string, read_only: true
|
|
50
|
+
attribute :block_storage, :string, read_only: true
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# @!attribute [r] databases
|
|
54
|
+
# @return [Integer]
|
|
55
|
+
# @!attribute [r] ip_addresses
|
|
56
|
+
# @return [Integer]
|
|
57
|
+
# @!attribute [r] prefixes
|
|
58
|
+
# @return [Integer]
|
|
59
|
+
# @!attribute [r] servers
|
|
60
|
+
# @return [Integer]
|
|
61
|
+
# @!attribute [r] storages
|
|
62
|
+
# @return [Integer]
|
|
63
|
+
# @!attribute [r] virtual_machines
|
|
64
|
+
# @return [Integer]
|
|
65
|
+
# @!attribute [r] vlans
|
|
66
|
+
# @return [Integer]
|
|
67
|
+
class Stats < APIObject
|
|
68
|
+
attribute :databases, :integer, read_only: true
|
|
69
|
+
attribute :ip_addresses, :integer, read_only: true
|
|
70
|
+
attribute :prefixes, :integer, read_only: true
|
|
71
|
+
attribute :servers, :integer, read_only: true
|
|
72
|
+
attribute :storages, :integer, read_only: true
|
|
73
|
+
attribute :virtual_machines, :integer, read_only: true
|
|
74
|
+
attribute :vlans, :integer, read_only: true
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# @!attribute [r] id
|
|
78
|
+
# @return [String]
|
|
79
|
+
# @!attribute [r] customer_billing_id
|
|
80
|
+
# @return [String]
|
|
81
|
+
# @!attribute [r] subscription_id
|
|
82
|
+
# @return [String]
|
|
83
|
+
# @!attribute [r] type
|
|
84
|
+
# @return [String]
|
|
85
|
+
# @!attribute [r] method
|
|
86
|
+
# @return [String]
|
|
87
|
+
class Billing < APIObject
|
|
88
|
+
attribute :id, :string, read_only: true
|
|
89
|
+
attribute :customer_billing_id, :string, read_only: true
|
|
90
|
+
attribute :subscription_id, :string, read_only: true
|
|
91
|
+
attribute :type, :string, read_only: true
|
|
92
|
+
attribute :method, :string, read_only: true
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# @!attribute [r] id
|
|
96
|
+
# @return [String]
|
|
97
|
+
# @!attribute [r] name
|
|
98
|
+
# @return [String]
|
|
99
|
+
# @!attribute [r] created_at
|
|
100
|
+
# @return [Time]
|
|
101
|
+
# @!attribute [r] updated_at
|
|
102
|
+
# @return [Time]
|
|
103
|
+
class Role < APIObject
|
|
104
|
+
attribute :id, :string, read_only: true
|
|
105
|
+
attribute :name, :string, read_only: true
|
|
106
|
+
attribute :created_at, :time, read_only: true
|
|
107
|
+
attribute :updated_at, :time, read_only: true
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# @!attribute [r] id
|
|
111
|
+
# @return [String]
|
|
112
|
+
# @!attribute [r] name
|
|
113
|
+
# @return [String]
|
|
114
|
+
# @!attribute [r] description
|
|
115
|
+
# @return [String]
|
|
116
|
+
# @!attribute [r] color
|
|
117
|
+
# @return [String]
|
|
118
|
+
class Tag < APIObject
|
|
119
|
+
attribute :id, :string, read_only: true
|
|
120
|
+
attribute :name, :string, read_only: true
|
|
121
|
+
attribute :description, :string, read_only: true
|
|
122
|
+
attribute :color, :string, read_only: true
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# @!attribute [r] id
|
|
126
|
+
# @return [String]
|
|
127
|
+
# @!attribute [r] name
|
|
128
|
+
# @return [String]
|
|
129
|
+
# @!attribute [r] slug
|
|
130
|
+
# @return [String]
|
|
131
|
+
# @!attribute [r] description
|
|
132
|
+
# @return [String]
|
|
133
|
+
# @!attribute [r] address
|
|
134
|
+
# @return [String]
|
|
135
|
+
# @!attribute [r] currency
|
|
136
|
+
# @return [Latitude::API::Objects::Currency]
|
|
137
|
+
# @!attribute [r] status
|
|
138
|
+
# @return [String]
|
|
139
|
+
# @!attribute [r] feature_flags
|
|
140
|
+
# @return [Array]
|
|
141
|
+
# @!attribute [r] limits
|
|
142
|
+
# @return [Latitude::API::Objects::Limits]
|
|
143
|
+
class Team < APIObject
|
|
144
|
+
attribute :id, :string, read_only: true
|
|
145
|
+
attribute :name, :string, read_only: true
|
|
146
|
+
attribute :slug, :string, read_only: true
|
|
147
|
+
attribute :description, :string, read_only: true
|
|
148
|
+
attribute :address, :string, read_only: true
|
|
149
|
+
attribute :currency, Objects::Currency, read_only: true
|
|
150
|
+
attribute :status, :string, read_only: true
|
|
151
|
+
attribute :feature_flags, :array, read_only: true
|
|
152
|
+
attribute :limits, Objects::Limits, read_only: true
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# @!attribute [r] id
|
|
156
|
+
# @return [String]
|
|
157
|
+
# @!attribute [r] slug
|
|
158
|
+
# @return [String]
|
|
159
|
+
# @!attribute [r] name
|
|
160
|
+
# @return [String]
|
|
161
|
+
# @!attribute [r] description
|
|
162
|
+
# @return [String]
|
|
163
|
+
# @!attribute [r] provisioning_type
|
|
164
|
+
# @return [String]
|
|
165
|
+
# @!attribute [r] billing_type
|
|
166
|
+
# @return [String]
|
|
167
|
+
# @!attribute [r] billing_method
|
|
168
|
+
# @return [String]
|
|
169
|
+
# @!attribute [r] bandwidth_alert
|
|
170
|
+
# @return [Boolean]
|
|
171
|
+
# @!attribute [r] environment
|
|
172
|
+
# @return [String]
|
|
173
|
+
# @!attribute [r] billing
|
|
174
|
+
# @return [Latitude::API::Objects::Billing]
|
|
175
|
+
# @!attribute [r] stats
|
|
176
|
+
# @return [Latitude::API::Objects::Stats]
|
|
177
|
+
class Project < APIObject
|
|
178
|
+
attribute :id, :string, read_only: true
|
|
179
|
+
attribute :slug, :string, read_only: true
|
|
180
|
+
attribute :name, :string, read_only: true
|
|
181
|
+
attribute :description, :string, read_only: true
|
|
182
|
+
attribute :provisioning_type, :string, read_only: true
|
|
183
|
+
attribute :billing_type, :string, read_only: true
|
|
184
|
+
attribute :billing_method, :string, read_only: true
|
|
185
|
+
attribute :bandwidth_alert, :boolean, read_only: true
|
|
186
|
+
attribute :environment, :string, read_only: true
|
|
187
|
+
attribute :billing, Objects::Billing, read_only: true
|
|
188
|
+
attribute :stats, Objects::Stats, read_only: true
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# @!attribute [r] id
|
|
192
|
+
# @return [String]
|
|
193
|
+
# @!attribute [r] email
|
|
194
|
+
# @return [String]
|
|
195
|
+
# @!attribute [r] first_name
|
|
196
|
+
# @return [String]
|
|
197
|
+
# @!attribute [r] last_name
|
|
198
|
+
# @return [String]
|
|
199
|
+
# @!attribute [r] created_at
|
|
200
|
+
# @return [Time]
|
|
201
|
+
# @!attribute [r] updated_at
|
|
202
|
+
# @return [Time]
|
|
203
|
+
# @!attribute [r] role
|
|
204
|
+
# @return [Latitude::API::Objects::Role]
|
|
205
|
+
class User < APIObject
|
|
206
|
+
attribute :id, :string, read_only: true
|
|
207
|
+
attribute :email, :string, read_only: true
|
|
208
|
+
attribute :first_name, :string, read_only: true
|
|
209
|
+
attribute :last_name, :string, read_only: true
|
|
210
|
+
attribute :created_at, :time, read_only: true
|
|
211
|
+
attribute :updated_at, :time, read_only: true
|
|
212
|
+
attribute :role, Objects::Role, read_only: true
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Latitude
|
|
4
|
+
module API
|
|
5
|
+
module Operations
|
|
6
|
+
module Action
|
|
7
|
+
def action(name, path:, type: nil, method: :post, attributes: {}, accepts: nil, returns: :raw)
|
|
8
|
+
action_method = method.to_sym
|
|
9
|
+
action_path = path
|
|
10
|
+
action_type = type
|
|
11
|
+
action_attrs = attributes
|
|
12
|
+
accepts_keys = accepts ? Array(accepts).map(&:to_sym) : nil
|
|
13
|
+
returns_mode = returns
|
|
14
|
+
|
|
15
|
+
define_singleton_method(name) do |id, params = {}, opts = {}|
|
|
16
|
+
params = params.dup
|
|
17
|
+
path_params = extract_path_params(params, path_param_keys)
|
|
18
|
+
if accepts_keys
|
|
19
|
+
unknown = params.keys.map(&:to_sym) - accepts_keys
|
|
20
|
+
raise ArgumentError, "unknown action params: #{unknown.join(', ')}" unless unknown.empty?
|
|
21
|
+
end
|
|
22
|
+
body_attrs = action_attrs.merge(params)
|
|
23
|
+
body = if action_type
|
|
24
|
+
JSONAPI.encode(type: action_type, attributes: body_attrs)
|
|
25
|
+
elsif !body_attrs.empty?
|
|
26
|
+
JSON.generate(body_attrs)
|
|
27
|
+
end
|
|
28
|
+
url = "#{instance_url(id, path_params: path_params)}/#{action_path}"
|
|
29
|
+
parsed = execute_request(method: action_method, path: url, body: body, opts: opts)
|
|
30
|
+
returns_mode == :instance ? construct_from(parsed, opts: opts, path_params: path_params) : parsed
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
define_method(name) do |params = {}, opts = {}|
|
|
34
|
+
merged = @path_params.merge(params || {})
|
|
35
|
+
self.class.public_send(name, id, merged, opts)
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Latitude
|
|
4
|
+
module API
|
|
5
|
+
module Operations
|
|
6
|
+
module Create
|
|
7
|
+
def create(params = {}, opts = {})
|
|
8
|
+
params = params.dup
|
|
9
|
+
path_params = extract_path_params(params, path_param_keys)
|
|
10
|
+
body = JSONAPI.encode(type: resource_type, attributes: params)
|
|
11
|
+
parsed = execute_request(
|
|
12
|
+
method: :post,
|
|
13
|
+
path: class_url(path_params: path_params),
|
|
14
|
+
body: body,
|
|
15
|
+
opts: opts,
|
|
16
|
+
)
|
|
17
|
+
construct_from(parsed, opts: opts, path_params: path_params)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Latitude
|
|
4
|
+
module API
|
|
5
|
+
module Operations
|
|
6
|
+
module Delete
|
|
7
|
+
def delete(id, params = {}, opts = {})
|
|
8
|
+
params = params.dup
|
|
9
|
+
path_params = extract_path_params(params, path_param_keys)
|
|
10
|
+
query = RequestParams.encode(params)
|
|
11
|
+
execute_request(
|
|
12
|
+
method: :delete,
|
|
13
|
+
path: instance_url(id, path_params: path_params),
|
|
14
|
+
query: query,
|
|
15
|
+
opts: opts,
|
|
16
|
+
)
|
|
17
|
+
true
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Latitude
|
|
4
|
+
module API
|
|
5
|
+
module Operations
|
|
6
|
+
module List
|
|
7
|
+
def list(params = {}, opts = {})
|
|
8
|
+
params = deep_dup_hash(params)
|
|
9
|
+
path_params = params.delete(:_path_params) || extract_path_params(params, path_param_keys)
|
|
10
|
+
query = build_query(params)
|
|
11
|
+
parsed = execute_request(
|
|
12
|
+
method: :get,
|
|
13
|
+
path: class_url(path_params: path_params),
|
|
14
|
+
query: query,
|
|
15
|
+
opts: opts,
|
|
16
|
+
)
|
|
17
|
+
ListObject.new(
|
|
18
|
+
resource_class: self,
|
|
19
|
+
parsed: parsed,
|
|
20
|
+
request_params: params,
|
|
21
|
+
request_opts: opts,
|
|
22
|
+
path_params: path_params,
|
|
23
|
+
)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def all(params = {}, opts = {})
|
|
27
|
+
list(params, opts).auto_paging_each.to_a
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def build_query(params)
|
|
33
|
+
params = params.dup
|
|
34
|
+
sort = params.delete(:sort) || params.delete("sort")
|
|
35
|
+
encoded = RequestParams.encode(params)
|
|
36
|
+
encoded_sort = RequestParams.encode_sort(sort)
|
|
37
|
+
[encoded, encoded_sort && !encoded_sort.empty? ? "sort=#{URI.encode_www_form_component(encoded_sort)}" : nil]
|
|
38
|
+
.reject { |s| s.nil? || s.empty? }
|
|
39
|
+
.join("&")
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def deep_dup_hash(hash)
|
|
43
|
+
hash.each_with_object({}) do |(k, v), out|
|
|
44
|
+
out[k] = case v
|
|
45
|
+
when Hash then deep_dup_hash(v)
|
|
46
|
+
when Array then v.dup
|
|
47
|
+
else v
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Latitude
|
|
4
|
+
module API
|
|
5
|
+
module Operations
|
|
6
|
+
module Nested
|
|
7
|
+
def nested_resource(name, class_name:)
|
|
8
|
+
parent_key = path_param_keys_for_parent
|
|
9
|
+
define_singleton_method(name) do |id|
|
|
10
|
+
klass = Object.const_get(class_name)
|
|
11
|
+
NestedAccessor.new(klass: klass, path_params: { parent_key => id })
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
define_method(name) do
|
|
15
|
+
klass = Object.const_get(class_name)
|
|
16
|
+
NestedAccessor.new(klass: klass, path_params: { parent_key => @id })
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def path_param_keys_for_parent
|
|
21
|
+
"#{resource_type.sub(/s\z/, '')}_id".to_sym
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
class NestedAccessor
|
|
26
|
+
def initialize(klass:, path_params:)
|
|
27
|
+
@klass = klass
|
|
28
|
+
@path_params = path_params
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def list(params = {}, opts = {})
|
|
32
|
+
@klass.list(params.merge(@path_params), opts, path_params: @path_params)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def retrieve(id, params = {}, opts = {})
|
|
36
|
+
@klass.retrieve(id, params.merge(@path_params), opts)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def create(params = {}, opts = {})
|
|
40
|
+
@klass.create(params.merge(@path_params), opts)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def update(id, params = {}, opts = {})
|
|
44
|
+
@klass.update(id, params.merge(@path_params), opts)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def delete(id, params = {}, opts = {})
|
|
48
|
+
@klass.delete(id, params.merge(@path_params), opts)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Latitude
|
|
4
|
+
module API
|
|
5
|
+
module Operations
|
|
6
|
+
module Retrieve
|
|
7
|
+
def retrieve(id, params = {}, opts = {})
|
|
8
|
+
params = params.dup
|
|
9
|
+
path_params = extract_path_params(params, path_param_keys)
|
|
10
|
+
query = RequestParams.encode(params)
|
|
11
|
+
parsed = execute_request(
|
|
12
|
+
method: :get,
|
|
13
|
+
path: instance_url(id, path_params: path_params),
|
|
14
|
+
query: query,
|
|
15
|
+
opts: opts,
|
|
16
|
+
)
|
|
17
|
+
construct_from(parsed, opts: opts, path_params: path_params)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Latitude
|
|
4
|
+
module API
|
|
5
|
+
module Operations
|
|
6
|
+
module Update
|
|
7
|
+
def update(id, params = {}, opts = {})
|
|
8
|
+
params = params.dup
|
|
9
|
+
path_params = extract_path_params(params, path_param_keys)
|
|
10
|
+
body = JSONAPI.encode(type: resource_type, id: id, attributes: params)
|
|
11
|
+
parsed = execute_request(
|
|
12
|
+
method: :patch,
|
|
13
|
+
path: instance_url(id, path_params: path_params),
|
|
14
|
+
body: body,
|
|
15
|
+
opts: opts,
|
|
16
|
+
)
|
|
17
|
+
construct_from(parsed, opts: opts, path_params: path_params)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "httparty"
|
|
4
|
+
|
|
5
|
+
module Latitude
|
|
6
|
+
module API
|
|
7
|
+
class RequestExecutor
|
|
8
|
+
RETRIABLE_STATUSES = [429, 500, 502, 503, 504].freeze
|
|
9
|
+
RETRIABLE_EXCEPTIONS = [
|
|
10
|
+
EOFError,
|
|
11
|
+
Errno::ECONNREFUSED,
|
|
12
|
+
Errno::ECONNRESET,
|
|
13
|
+
Errno::EPIPE,
|
|
14
|
+
Net::OpenTimeout,
|
|
15
|
+
Net::ReadTimeout,
|
|
16
|
+
OpenSSL::SSL::SSLError,
|
|
17
|
+
SocketError,
|
|
18
|
+
].freeze
|
|
19
|
+
|
|
20
|
+
def initialize(config)
|
|
21
|
+
@config = config
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def execute(method:, path:, query: nil, body: nil, headers: {}, idempotency_key: nil, extra_headers: {})
|
|
25
|
+
@config.validate_api_key!
|
|
26
|
+
|
|
27
|
+
url = build_url(path, query)
|
|
28
|
+
request_headers = build_headers(method, headers, idempotency_key, extra_headers)
|
|
29
|
+
options = build_httparty_options(request_headers, body)
|
|
30
|
+
|
|
31
|
+
attempts = 0
|
|
32
|
+
max_retries = @config.max_network_retries
|
|
33
|
+
|
|
34
|
+
loop do
|
|
35
|
+
attempts += 1
|
|
36
|
+
started = Time.now
|
|
37
|
+
response = nil
|
|
38
|
+
begin
|
|
39
|
+
response = HTTParty.send(method.to_sym, url.to_s, options)
|
|
40
|
+
rescue *RETRIABLE_EXCEPTIONS => e
|
|
41
|
+
log_exception(e, method, path, attempts)
|
|
42
|
+
raise ConnectionError.new("Network error talking to #{url}: #{e.message}", cause: e) if attempts > max_retries
|
|
43
|
+
|
|
44
|
+
sleep(backoff_delay(attempts, nil))
|
|
45
|
+
next
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
duration = ((Time.now - started) * 1000).round
|
|
49
|
+
log_response(method, path, response, duration)
|
|
50
|
+
|
|
51
|
+
if retriable_status?(response.code) && attempts <= max_retries
|
|
52
|
+
sleep(backoff_delay(attempts, response))
|
|
53
|
+
next
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
return handle_response(response, method: method, path: path)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
def build_url(path, query)
|
|
63
|
+
base = URI.parse(@config.api_base)
|
|
64
|
+
resolved = path.start_with?("http://", "https://") ? URI.parse(path) : URI.join("#{base}/", path.sub(%r{^/}, ""))
|
|
65
|
+
if query && !query.empty?
|
|
66
|
+
resolved.query = [resolved.query, query].compact.reject(&:empty?).join("&")
|
|
67
|
+
end
|
|
68
|
+
resolved
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def build_headers(method, headers, idempotency_key, extra_headers)
|
|
72
|
+
h = {}
|
|
73
|
+
h["Authorization"] = "Bearer #{@config.api_key}"
|
|
74
|
+
h["Accept"] = JSONAPI::MEDIA_TYPE
|
|
75
|
+
h["Content-Type"] = "application/json" if %i[post put patch].include?(method.to_sym)
|
|
76
|
+
h["User-Agent"] = user_agent
|
|
77
|
+
h["API-Version"] = @config.api_version if @config.api_version
|
|
78
|
+
h["Idempotency-Key"] = idempotency_key if idempotency_key
|
|
79
|
+
|
|
80
|
+
(headers || {}).each { |k, v| h[k.to_s] = v.to_s }
|
|
81
|
+
(extra_headers || {}).each { |k, v| h[k.to_s] = v.to_s }
|
|
82
|
+
h
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def build_httparty_options(headers, body)
|
|
86
|
+
options = {
|
|
87
|
+
headers: headers,
|
|
88
|
+
open_timeout: @config.open_timeout,
|
|
89
|
+
read_timeout: @config.read_timeout,
|
|
90
|
+
follow_redirects: false,
|
|
91
|
+
format: :plain,
|
|
92
|
+
}
|
|
93
|
+
options[:write_timeout] = @config.write_timeout if @config.write_timeout
|
|
94
|
+
options[:verify] = @config.verify_ssl_certs
|
|
95
|
+
options[:ca_path] = @config.ca_bundle_path if @config.ca_bundle_path
|
|
96
|
+
options[:body] = body if body
|
|
97
|
+
|
|
98
|
+
if @config.proxy
|
|
99
|
+
proxy_uri = URI.parse(@config.proxy)
|
|
100
|
+
options[:http_proxyaddr] = proxy_uri.host
|
|
101
|
+
options[:http_proxyport] = proxy_uri.port
|
|
102
|
+
options[:http_proxyuser] = proxy_uri.user if proxy_uri.user
|
|
103
|
+
options[:http_proxypass] = proxy_uri.password if proxy_uri.password
|
|
104
|
+
end
|
|
105
|
+
options
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def handle_response(response, method:, path:)
|
|
109
|
+
status = response.code
|
|
110
|
+
body = response.body
|
|
111
|
+
headers = normalize_headers(response.headers)
|
|
112
|
+
request_id = headers["x-request-id"] || headers["request-id"]
|
|
113
|
+
|
|
114
|
+
if status.between?(200, 299)
|
|
115
|
+
return nil if body.nil? || body.empty? || status == 204
|
|
116
|
+
|
|
117
|
+
return JSONAPI.parse(body)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
parsed = begin
|
|
121
|
+
JSONAPI.parse(body)
|
|
122
|
+
rescue StandardError
|
|
123
|
+
nil
|
|
124
|
+
end
|
|
125
|
+
klass = Latitude::API.error_class_for_status(status)
|
|
126
|
+
raise klass.new(
|
|
127
|
+
nil,
|
|
128
|
+
http_status: status,
|
|
129
|
+
http_headers: headers,
|
|
130
|
+
http_body: body,
|
|
131
|
+
json_body: parsed,
|
|
132
|
+
request_id: request_id,
|
|
133
|
+
)
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def normalize_headers(headers)
|
|
137
|
+
return {} if headers.nil?
|
|
138
|
+
|
|
139
|
+
result = {}
|
|
140
|
+
headers.each { |k, v| result[k.to_s.downcase] = v.is_a?(Array) ? v.first : v }
|
|
141
|
+
result
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def retriable_status?(status)
|
|
145
|
+
RETRIABLE_STATUSES.include?(status.to_i)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def backoff_delay(attempt, response)
|
|
149
|
+
if response && response.code.to_i == 429
|
|
150
|
+
retry_after = extract_retry_after(response)
|
|
151
|
+
return retry_after.to_f if retry_after
|
|
152
|
+
end
|
|
153
|
+
base = @config.initial_network_retry_delay
|
|
154
|
+
max = @config.max_network_retry_delay
|
|
155
|
+
delay = base * (2**(attempt - 1))
|
|
156
|
+
jitter = delay * (rand * 0.5)
|
|
157
|
+
[max, delay + jitter].min
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def extract_retry_after(response)
|
|
161
|
+
headers = normalize_headers(response.headers)
|
|
162
|
+
header = headers["retry-after"]
|
|
163
|
+
return Integer(header) if header && header.to_s.match?(/\A\d+\z/)
|
|
164
|
+
|
|
165
|
+
body = response.body
|
|
166
|
+
return nil if body.nil? || body.empty?
|
|
167
|
+
|
|
168
|
+
parsed = begin
|
|
169
|
+
JSONAPI.parse(body)
|
|
170
|
+
rescue StandardError
|
|
171
|
+
nil
|
|
172
|
+
end
|
|
173
|
+
errors = parsed.is_a?(Hash) ? Array(parsed["errors"]) : []
|
|
174
|
+
errors.each do |e|
|
|
175
|
+
meta = e["meta"] || {}
|
|
176
|
+
return Integer(meta["retry_after"]) if meta["retry_after"]
|
|
177
|
+
end
|
|
178
|
+
nil
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def user_agent
|
|
182
|
+
parts = ["Latitude/v1 RubyBindings/#{VERSION}"]
|
|
183
|
+
parts << "(#{RUBY_ENGINE} #{RUBY_VERSION}; #{RUBY_PLATFORM})"
|
|
184
|
+
if (info = @config.app_info)
|
|
185
|
+
app = +"#{info[:name]}"
|
|
186
|
+
app << "/#{info[:version]}" if info[:version]
|
|
187
|
+
app << " (#{info[:url]})" if info[:url]
|
|
188
|
+
parts << app
|
|
189
|
+
end
|
|
190
|
+
parts.join(" ")
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def log_response(method, path, response, duration_ms)
|
|
194
|
+
logger = @config.logger
|
|
195
|
+
return unless logger
|
|
196
|
+
|
|
197
|
+
logger.info("latitude-api #{method.to_s.upcase} #{path} status=#{response.code} dur=#{duration_ms}ms")
|
|
198
|
+
return unless logger.debug?
|
|
199
|
+
|
|
200
|
+
parsed = begin
|
|
201
|
+
JSON.parse(response.body.to_s)
|
|
202
|
+
rescue StandardError
|
|
203
|
+
response.body
|
|
204
|
+
end
|
|
205
|
+
redacted = Util.redact(parsed, @config.sensitive_fields)
|
|
206
|
+
logger.debug("latitude-api response: #{redacted.inspect[0..4096]}")
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def log_exception(exception, method, path, attempt)
|
|
210
|
+
logger = @config.logger
|
|
211
|
+
return unless logger
|
|
212
|
+
|
|
213
|
+
logger.warn("latitude-api #{method.to_s.upcase} #{path} attempt=#{attempt} error=#{exception.class}: #{exception.message}")
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
end
|