katalyst-google-apis 1.2.2 → 1.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '068ff7b04cb4fdb9b471bbb8bbd4dcef65de8005a4b1d96675119d3293745782'
4
- data.tar.gz: '02933b3e5f96569817fdfdd04d24691bd6970d5515c6ab7ed979f9c84e07e4f4'
3
+ metadata.gz: 249511e97a7e28426cbf5ad750c3c77c1dc5f882c885a37868537bfea975a9e7
4
+ data.tar.gz: 86758221a5cdc2a554d767a7e126b1acd48d3034b87100e23dd88876e291e713
5
5
  SHA512:
6
- metadata.gz: 516aa02ce48141da7ec32d79122f055e1feb6ae7fb3edf6965035e6dac0df840288c6955d615b6729c3cfc131507748bc509bbc8afb993ee251955243d804d56
7
- data.tar.gz: 9aa73f7657243263513eaab6ce323823d95b9e8b3661f7e71c6bf5ddafd1d3ad668d1b99dfad6e6a30076a43220a428be9862f69a7c6e3ca35e696dff9f8e26c
6
+ metadata.gz: da31ead0972e8152ab79e04c2a18ed875dcc367187f8ee03a60f7d89337d85956f95f944f524852eddd8fbe940da4e45884bd39dae471d499134a19faaccea59
7
+ data.tar.gz: ece80e3217d17d22c28f8b787c10ad843200b3c322b6dd1427b2a13ef3bdcbd4df484adeccef53ef83c5d12d104bbfc012fd0a49e39a5834e4a7dda7b71e393a
@@ -7,14 +7,20 @@ module Katalyst
7
7
  class GenerateContentService
8
8
  attr_reader :response, :result, :error, :content_text
9
9
 
10
- def self.call(parent:, model:, payload:, credentials: Katalyst::GoogleApis.credentials)
11
- new(parent:, model:, credentials:).call(payload:)
10
+ def self.call(parent:, model:, payload:,
11
+ credentials: Katalyst::GoogleApis.credentials,
12
+ retries: 5,
13
+ jitter: 1_000)
14
+ new(parent:, model:, credentials:, attempt: 0, retries:, jitter:).call(payload:)
12
15
  end
13
16
 
14
- def initialize(credentials:, model:, parent:)
17
+ def initialize(credentials:, model:, parent:, attempt:, retries:, jitter:)
15
18
  @credentials = credentials
16
19
  @model = model
17
20
  @parent = parent
21
+ @attempt = attempt
22
+ @retries = retries
23
+ @jitter = jitter
18
24
  end
19
25
 
20
26
  def call(payload:)
@@ -34,7 +40,7 @@ module Katalyst
34
40
  end
35
41
 
36
42
  if result[:error].present?
37
- @error = GoogleApis::Error.new(**result[:error])
43
+ raise GoogleApis::Error.new(**result[:error])
38
44
  else
39
45
  @content_text = result.dig(:candidates, 0, :content, :parts, 0, :text)
40
46
  end
@@ -42,7 +48,7 @@ module Katalyst
42
48
  self
43
49
  rescue StandardError => e
44
50
  @error = e
45
- raise e
51
+ retry?(e) ? retry : raise
46
52
  ensure
47
53
  report_error
48
54
  end
@@ -84,11 +90,40 @@ module Katalyst
84
90
  data: {
85
91
  url:,
86
92
  method: "POST",
87
- status_code: error.code,
93
+ status_code: error.try(:code),
88
94
  reason: error.message,
89
95
  },
90
96
  )
91
97
  end
98
+
99
+ def retry?(error)
100
+ if error.is_a?(GoogleApis::Error) && error.code == 429 && @attempt < @retries
101
+ Kernel.sleep(backoff)
102
+
103
+ @response = nil
104
+ @result = nil
105
+ @error = nil
106
+ @attempt += 1
107
+ true
108
+ else
109
+ false
110
+ end
111
+ end
112
+
113
+ def backoff
114
+ [@jitter, response_headers.fetch(:retry_after, 0).to_i * 1000].max + (@jitter * rand)
115
+ end
116
+
117
+ def response_headers
118
+ headers = {}
119
+ @response.header_str.each_line do |line|
120
+ key, value = line.split(":", 2)
121
+ next unless value
122
+
123
+ headers[key.underscore.to_sym] = value.strip
124
+ end
125
+ headers
126
+ end
92
127
  end
93
128
  end
94
129
  end
@@ -0,0 +1,119 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "curb"
4
+
5
+ module Katalyst
6
+ module GoogleApis
7
+ module Geocoding
8
+ # Use Google Maps Geocoding API to find a location from lat/lng.
9
+ class ReverseService
10
+ attr_reader :response, :result, :error
11
+
12
+ def self.scope
13
+ "https://www.googleapis.com/auth/maps-platform.geocode.location"
14
+ end
15
+
16
+ def self.call(latlng:, credentials: Katalyst::GoogleApis.credentials(scope:))
17
+ new(credentials:).call(latlng:)
18
+ end
19
+
20
+ def initialize(credentials:)
21
+ @credentials = credentials
22
+ end
23
+
24
+ def call(latlng:)
25
+ @latlng = latlng
26
+
27
+ @response = Curl.get(url) do |http|
28
+ http.headers["Content-Type"] = "application/json; UTF-8"
29
+ @credentials.apply!(http.headers)
30
+ end
31
+
32
+ if %r{^application/json}.match?(@response.content_type)
33
+ @result = JSON.parse(response.body, symbolize_names: true)
34
+ else
35
+ raise GoogleApis::Error.new(
36
+ code: response.response_code,
37
+ status: Rack::Utils::HTTP_STATUS_CODES[response.response_code],
38
+ message: "Unexpected HTTP response received (#{response.response_code}, #{@response.content_type})",
39
+ )
40
+ end
41
+
42
+ if result[:error].present?
43
+ api_error = result.fetch(:error)
44
+
45
+ raise GoogleApis::Error.new(
46
+ code: api_error.fetch(:code, response.response_code),
47
+ status: api_error.fetch(:status, Rack::Utils::HTTP_STATUS_CODES[response.response_code]),
48
+ message: api_error.fetch(:message, "Unexpected API error"),
49
+ details: api_error.fetch(:details, nil),
50
+ )
51
+ end
52
+
53
+ self
54
+ rescue StandardError => e
55
+ @error = e
56
+ raise
57
+ ensure
58
+ report_error
59
+ end
60
+
61
+ def locations
62
+ @result&.fetch(:results, nil)
63
+ end
64
+
65
+ def first_location
66
+ locations&.first
67
+ end
68
+
69
+ def formatted_address
70
+ first_location&.dig(:formattedAddress)
71
+ end
72
+
73
+ def latlng
74
+ location = first_location&.dig(:location)
75
+ return if location.blank?
76
+
77
+ latitude = location[:latitude]
78
+ longitude = location[:longitude]
79
+ return if latitude.nil? || longitude.nil?
80
+
81
+ [latitude, longitude].join(",")
82
+ end
83
+
84
+ def inspect
85
+ "#<#{self.class.name} result: #{@result.inspect} error: #{@error.inspect}>"
86
+ end
87
+
88
+ private
89
+
90
+ def url
91
+ "https://geocode.googleapis.com/v4beta/geocode/location/#{@latlng}"
92
+ end
93
+
94
+ def report_error
95
+ return if error.nil?
96
+
97
+ if defined?(Sentry)
98
+ Sentry.add_breadcrumb(sentry_breadcrumb(error))
99
+ else
100
+ Rails.logger.error(error)
101
+ end
102
+ end
103
+
104
+ def sentry_breadcrumb(error)
105
+ Sentry::Breadcrumb.new(
106
+ type: "http",
107
+ category: "geocode",
108
+ data: {
109
+ url:,
110
+ method: "GET",
111
+ status_code: error.try(:code),
112
+ reason: error.message,
113
+ },
114
+ )
115
+ end
116
+ end
117
+ end
118
+ end
119
+ end
@@ -0,0 +1,134 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "curb"
4
+
5
+ module Katalyst
6
+ module GoogleApis
7
+ module Geocoding
8
+ # Use Google Maps Geocoding API to find a location from an address.
9
+ class SearchService
10
+ attr_reader :response, :result, :error
11
+
12
+ def self.scope
13
+ "https://www.googleapis.com/auth/maps-platform.geocode.address"
14
+ end
15
+
16
+ def self.call(address:, bounds:, credentials: Katalyst::GoogleApis.credentials(scope:))
17
+ new(credentials:).call(address:, bounds:)
18
+ end
19
+
20
+ def initialize(credentials:)
21
+ @credentials = credentials
22
+ end
23
+
24
+ def call(address:, bounds:)
25
+ @address = address
26
+ @bounds = bounds
27
+
28
+ @response = Curl.get(url, **params) do |http|
29
+ http.headers["Content-Type"] = "application/json; UTF-8"
30
+ @credentials.apply!(http.headers)
31
+ end
32
+
33
+ if %r{^application/json}.match?(@response.content_type)
34
+ @result = JSON.parse(response.body, symbolize_names: true)
35
+ else
36
+ raise GoogleApis::Error.new(
37
+ code: response.response_code,
38
+ status: Rack::Utils::HTTP_STATUS_CODES[response.response_code],
39
+ message: "Unexpected HTTP response received (#{response.response_code}, #{@response.content_type})",
40
+ )
41
+ end
42
+
43
+ if result[:error].present?
44
+ api_error = result.fetch(:error)
45
+
46
+ raise GoogleApis::Error.new(
47
+ code: api_error.fetch(:code, response.response_code),
48
+ status: api_error.fetch(:status, Rack::Utils::HTTP_STATUS_CODES[response.response_code]),
49
+ message: api_error.fetch(:message, "Unexpected API error"),
50
+ details: api_error.fetch(:details, nil),
51
+ )
52
+ end
53
+
54
+ self
55
+ rescue StandardError => e
56
+ @error = e
57
+ raise
58
+ ensure
59
+ report_error
60
+ end
61
+
62
+ def locations
63
+ @result&.fetch(:results, nil)
64
+ end
65
+
66
+ def first_location
67
+ locations&.first
68
+ end
69
+
70
+ def formatted_address
71
+ first_location&.dig(:formattedAddress)
72
+ end
73
+
74
+ def latlng
75
+ location = first_location&.dig(:location)
76
+ return if location.blank?
77
+
78
+ latitude = location[:latitude]
79
+ longitude = location[:longitude]
80
+ return if latitude.nil? || longitude.nil?
81
+
82
+ [latitude, longitude].join(",")
83
+ end
84
+
85
+ def inspect
86
+ "#<#{self.class.name} result: #{@result.inspect} error: #{@error.inspect}>"
87
+ end
88
+
89
+ private
90
+
91
+ def url
92
+ "https://geocode.googleapis.com/v4beta/geocode/address/#{URI.encode_uri_component(@address)}"
93
+ end
94
+
95
+ def params
96
+ low, high = @bounds.split("|")
97
+
98
+ low_lat, low_lng = low.split(",")
99
+ high_lat, high_lng = high.split(",")
100
+
101
+ {
102
+ "locationBias.rectangle.low.latitude" => low_lat,
103
+ "locationBias.rectangle.low.longitude" => low_lng,
104
+ "locationBias.rectangle.high.latitude" => high_lat,
105
+ "locationBias.rectangle.high.longitude" => high_lng,
106
+ }
107
+ end
108
+
109
+ def report_error
110
+ return if error.nil?
111
+
112
+ if defined?(Sentry)
113
+ Sentry.add_breadcrumb(sentry_breadcrumb(error))
114
+ else
115
+ Rails.logger.error(error)
116
+ end
117
+ end
118
+
119
+ def sentry_breadcrumb(error)
120
+ Sentry::Breadcrumb.new(
121
+ type: "http",
122
+ category: "geocode",
123
+ data: {
124
+ url:,
125
+ method: "GET",
126
+ status_code: error.try(:code),
127
+ reason: error.message,
128
+ },
129
+ )
130
+ end
131
+ end
132
+ end
133
+ end
134
+ end
@@ -14,7 +14,7 @@ module Katalyst
14
14
 
15
15
  def initialize(credentials:, parent:)
16
16
  @credentials = credentials
17
- @parent = parent
17
+ @parent = parent
18
18
  end
19
19
 
20
20
  def call(assessment:)
@@ -84,7 +84,7 @@ module Katalyst
84
84
  data: {
85
85
  url:,
86
86
  method: "POST",
87
- status_code: error.code,
87
+ status_code: error.try(:code),
88
88
  reason: error.message,
89
89
  },
90
90
  )
data/config/boot.rb ADDED
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ ENV["BUNDLE_GEMFILE"] ||= File.expand_path("../Gemfile", __dir__)
4
+
5
+ require "bundler/setup" # Set up gems listed in the Gemfile.
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: katalyst-google-apis
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.2
4
+ version: 1.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Katalyst Interactive
@@ -79,8 +79,11 @@ files:
79
79
  - app/services/katalyst/google_apis/credentials.rb
80
80
  - app/services/katalyst/google_apis/error.rb
81
81
  - app/services/katalyst/google_apis/gemini/generate_content_service.rb
82
+ - app/services/katalyst/google_apis/geocoding/reverse_service.rb
83
+ - app/services/katalyst/google_apis/geocoding/search_service.rb
82
84
  - app/services/katalyst/google_apis/recaptcha/assessment_service.rb
83
85
  - app/validators/recaptcha_validator.rb
86
+ - config/boot.rb
84
87
  - config/ci.rb
85
88
  - config/importmap.rb
86
89
  - config/locales/en.yml