pangea-sdk 0.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 (63) hide show
  1. checksums.yaml +7 -0
  2. data/.ignore +2 -0
  3. data/README.md +1 -0
  4. data/lib/pangea/client.rb +51 -0
  5. data/lib/pangea/errors.rb +113 -0
  6. data/lib/pangea/internal/transport/base_client.rb +346 -0
  7. data/lib/pangea/internal/transport/pooled_net_requester.rb +191 -0
  8. data/lib/pangea/internal/type/array_of.rb +119 -0
  9. data/lib/pangea/internal/type/base_model.rb +289 -0
  10. data/lib/pangea/internal/type/boolean.rb +44 -0
  11. data/lib/pangea/internal/type/converter.rb +228 -0
  12. data/lib/pangea/internal/type/hash_of.rb +166 -0
  13. data/lib/pangea/internal/type/request_parameters.rb +38 -0
  14. data/lib/pangea/internal/type/union.rb +66 -0
  15. data/lib/pangea/internal/type/unknown.rb +50 -0
  16. data/lib/pangea/internal/util.rb +429 -0
  17. data/lib/pangea/internal.rb +12 -0
  18. data/lib/pangea/models/ai_guard/classification_result.rb +33 -0
  19. data/lib/pangea/models/ai_guard/hardening_result.rb +27 -0
  20. data/lib/pangea/models/ai_guard/language_result.rb +20 -0
  21. data/lib/pangea/models/ai_guard/malicious_entity_result.rb +42 -0
  22. data/lib/pangea/models/ai_guard/prompt_injection_result.rb +33 -0
  23. data/lib/pangea/models/ai_guard/redact_entity_result.rb +43 -0
  24. data/lib/pangea/models/ai_guard/single_entity_result.rb +21 -0
  25. data/lib/pangea/models/ai_guard/text_guard_message_param.rb +19 -0
  26. data/lib/pangea/models/ai_guard/text_guard_params.rb +24 -0
  27. data/lib/pangea/models/ai_guard/text_guard_result.rb +308 -0
  28. data/lib/pangea/models/ai_guard/topic_result.rb +33 -0
  29. data/lib/pangea/models/pangea_response.rb +67 -0
  30. data/lib/pangea/request_options.rb +35 -0
  31. data/lib/pangea/services/ai_guard.rb +62 -0
  32. data/lib/pangea/version.rb +5 -0
  33. data/lib/pangea.rb +45 -0
  34. data/manifest.yaml +6 -0
  35. data/rbi/lib/pangea/client.rbi +25 -0
  36. data/rbi/lib/pangea/internal/internal.rbi +28 -0
  37. data/rbi/lib/pangea/internal/transport/base_client.rbi +18 -0
  38. data/rbi/lib/pangea/internal/type/array_of.rbi +66 -0
  39. data/rbi/lib/pangea/internal/type/base_model.rbi +33 -0
  40. data/rbi/lib/pangea/internal/type/boolean.rbi +46 -0
  41. data/rbi/lib/pangea/internal/type/converter.rbi +38 -0
  42. data/rbi/lib/pangea/internal/type/request_parameters.rbi +20 -0
  43. data/rbi/lib/pangea/internal/type/union.rbi +21 -0
  44. data/rbi/lib/pangea/internal/type/unknown.rbi +20 -0
  45. data/rbi/lib/pangea/internal.rbi +7 -0
  46. data/rbi/lib/pangea/models/ai_guard/text_guard_message_param.rbi +15 -0
  47. data/rbi/lib/pangea/models/ai_guard/text_guard_result.rbi +13 -0
  48. data/rbi/lib/pangea/models/pangea_response.rbi +31 -0
  49. data/rbi/lib/pangea/request_options.rbi +17 -0
  50. data/rbi/lib/pangea/services/ai_guard.rbi +28 -0
  51. data/rbi/lib/pangea/version.rbi +5 -0
  52. data/sig/pangea/client.rbs +12 -0
  53. data/sig/pangea/internal/transport/base_client.rbs +14 -0
  54. data/sig/pangea/internal/type/base_model.rbs +12 -0
  55. data/sig/pangea/internal/type/boolean.rbs +10 -0
  56. data/sig/pangea/internal/type/converter.rbs +9 -0
  57. data/sig/pangea/internal/type/request_parameters.rbs +15 -0
  58. data/sig/pangea/models/pangea_response.rbs +12 -0
  59. data/sig/pangea/models/text_guard_result.rbs +19 -0
  60. data/sig/pangea/request_options.rbs +34 -0
  61. data/sig/pangea/services/ai_guard.rbs +12 -0
  62. data/sig/pangea/version.rbs +3 -0
  63. metadata +126 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: d1b347bd784b3a0bf8ce40adf2089ecfd694df533aba07788be0991b848785ad
4
+ data.tar.gz: f0cbdaaab93f69991b538caa35144ddafb671673eae269d3399a57eca6921d81
5
+ SHA512:
6
+ metadata.gz: ddf4fe35807a1f473a37a670bb713c03f92272e535d31ac304ba9138b53a2c18701621d88bf6b25183d40dc674d47e0a8a64aabd0919283146bc87703d71855c
7
+ data.tar.gz: ef2d077e6725d10b75f932ff69c98845eba47585e934ce498836008585be5a9da3df43c779d154c7ff3360118a71d4afcc356206bccfa5e5a75e8b674fc43dd6
data/.ignore ADDED
@@ -0,0 +1,2 @@
1
+ rbi/*
2
+ sig/*
data/README.md ADDED
@@ -0,0 +1 @@
1
+ # Pangea Ruby SDK
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pangea
4
+ class Client < Pangea::Internal::Transport::BaseClient
5
+ # Default per-request timeout.
6
+ DEFAULT_TIMEOUT_IN_SECONDS = 600.0
7
+
8
+ # Default max number of retries to attempt after a failed retryable request.
9
+ DEFAULT_MAX_RETRIES = 2
10
+
11
+ # Default initial retry delay in seconds.
12
+ # Overall delay is calculated using exponential backoff + jitter.
13
+ DEFAULT_INITIAL_RETRY_DELAY = 0.5
14
+
15
+ # @return [String]
16
+ attr_reader :api_token
17
+
18
+ # @api private
19
+ #
20
+ # @return [Hash{String=>String}]
21
+ private def auth_headers
22
+ return {} if @api_token.nil?
23
+
24
+ {"authorization" => "Bearer #{@api_token}"}
25
+ end
26
+
27
+ # Creates and returns a new client for interacting with the Pangea API.
28
+ #
29
+ # @param api_token [String]
30
+ # @param base_url [String]
31
+ # @param max_retries [Integer] Max number of retries to attempt after a
32
+ # failed retryable request.
33
+ def initialize(
34
+ api_token:,
35
+ base_url:,
36
+ initial_retry_delay: DEFAULT_INITIAL_RETRY_DELAY,
37
+ max_retries: DEFAULT_MAX_RETRIES,
38
+ timeout: DEFAULT_TIMEOUT_IN_SECONDS
39
+ )
40
+ @api_token = api_token&.to_s
41
+
42
+ super(
43
+ base_url: base_url,
44
+ initial_retry_delay: initial_retry_delay,
45
+ max_retries: max_retries,
46
+ headers: {},
47
+ timeout: timeout
48
+ )
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pangea
4
+ module Errors
5
+ class Error < StandardError
6
+ # @!parse
7
+ # # @return [StandardError, nil]
8
+ # attr_accessor :cause
9
+ end
10
+
11
+ class ConversionError < Pangea::Errors::Error
12
+ end
13
+
14
+ class APIError < Pangea::Errors::Error
15
+ # @return [URI::Generic]
16
+ attr_accessor :url
17
+
18
+ # @return [Integer, nil]
19
+ attr_accessor :status
20
+
21
+ # @return [Object, nil]
22
+ attr_accessor :body
23
+
24
+ # @return [String, nil]
25
+ attr_accessor :code
26
+
27
+ # @return [String, nil]
28
+ attr_accessor :param
29
+
30
+ # @return [String, nil]
31
+ attr_accessor :type
32
+
33
+ # @api private
34
+ #
35
+ # @param url [URI::Generic]
36
+ # @param status [Integer, nil]
37
+ # @param body [Object, nil]
38
+ # @param request [nil]
39
+ # @param response [nil]
40
+ # @param message [String, nil]
41
+ def initialize(url:, status: nil, body: nil, request: nil, response: nil, message: nil)
42
+ @url = url
43
+ @status = status
44
+ @body = body
45
+ @request = request
46
+ @response = response
47
+ super(message)
48
+ end
49
+ end
50
+
51
+ class APIConnectionError < Pangea::Errors::APIError
52
+ # @!parse
53
+ # # @return [nil]
54
+ # attr_accessor :status
55
+
56
+ # @!parse
57
+ # # @return [nil]
58
+ # attr_accessor :body
59
+
60
+ # @!parse
61
+ # # @return [nil]
62
+ # attr_accessor :code
63
+
64
+ # @!parse
65
+ # # @return [nil]
66
+ # attr_accessor :param
67
+
68
+ # @!parse
69
+ # # @return [nil]
70
+ # attr_accessor :type
71
+
72
+ # @api private
73
+ #
74
+ # @param url [URI::Generic]
75
+ # @param status [nil]
76
+ # @param body [nil]
77
+ # @param request [nil]
78
+ # @param response [nil]
79
+ # @param message [String, nil]
80
+ def initialize(
81
+ url:,
82
+ status: nil,
83
+ body: nil,
84
+ request: nil,
85
+ response: nil,
86
+ message: "Connection error."
87
+ )
88
+ super
89
+ end
90
+ end
91
+
92
+ class APITimeoutError < Pangea::Errors::APIConnectionError
93
+ # @api private
94
+ #
95
+ # @param url [URI::Generic]
96
+ # @param status [nil]
97
+ # @param body [nil]
98
+ # @param request [nil]
99
+ # @param response [nil]
100
+ # @param message [String, nil]
101
+ def initialize(
102
+ url:,
103
+ status: nil,
104
+ body: nil,
105
+ request: nil,
106
+ response: nil,
107
+ message: "Request timed out."
108
+ )
109
+ super
110
+ end
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,346 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pangea
4
+ module Internal
5
+ module Transport
6
+ # @api private
7
+ #
8
+ # @abstract
9
+ class BaseClient
10
+ PLATFORM_HEADERS =
11
+ {
12
+ "user-agent" => "pangea-ruby/#{Pangea::VERSION}"
13
+ }
14
+
15
+ class << self
16
+ # @api private
17
+ #
18
+ # @return [Hash{String=>String}]
19
+ private def auth_headers = {}
20
+
21
+ # @api private
22
+ #
23
+ # @param status [Integer, Pangea::Errors::APIConnectionError]
24
+ # @param stream [Enumerable<String>, nil]
25
+ def reap_connection!(status, stream:)
26
+ case status
27
+ in (..199) | (300..499)
28
+ stream&.each { next }
29
+ in Pangea::Errors::APIConnectionError | (500..)
30
+ Pangea::Internal::Util.close_fused!(stream)
31
+ else
32
+ end
33
+ end
34
+
35
+ # @api private
36
+ #
37
+ # @param req [Hash{Symbol=>Object}]
38
+ #
39
+ # @raise [ArgumentError]
40
+ def validate!(req)
41
+ keys = [:method, :path, :query, :headers, :body, :unwrap, :page, :structure, :model, :options]
42
+ case req
43
+ in Hash
44
+ req.each_key do |k|
45
+ unless keys.include?(k)
46
+ raise ArgumentError.new("Request `req` keys must be one of #{keys}, got #{k.inspect}")
47
+ end
48
+ end
49
+ else
50
+ raise ArgumentError.new("Request `req` must be a Hash or RequestOptions, got #{req.inspect}")
51
+ end
52
+ end
53
+ end
54
+
55
+ # @api private
56
+ # @return [Pangea::Internal::Transport::PooledNetRequester]
57
+ attr_accessor :requester
58
+
59
+ # @api private
60
+ #
61
+ # @param base_url [String]
62
+ # @param headers [Hash{String=>String, Integer, Array<String, Integer, nil>, nil}]
63
+ # @param initial_retry_delay [Float]
64
+ # @param max_retries [Integer]
65
+ # @param timeout [Float]
66
+ def initialize(
67
+ base_url:,
68
+ headers: {},
69
+ initial_retry_delay: 0.0,
70
+ max_retries: 0,
71
+ timeout: 0.0
72
+ )
73
+ @requester = Pangea::Internal::Transport::PooledNetRequester.new
74
+
75
+ @base_url = Pangea::Internal::Util.parse_uri(base_url)
76
+ @headers = Pangea::Internal::Util.normalized_headers(
77
+ self.class::PLATFORM_HEADERS,
78
+ {
79
+ "accept" => "application/json",
80
+ "content-type" => "application/json"
81
+ },
82
+ headers
83
+ )
84
+ @initial_retry_delay = initial_retry_delay
85
+ @max_retries = max_retries
86
+ @timeout = timeout
87
+ end
88
+
89
+ # @api private
90
+ #
91
+ # @return [Hash{String=>String}]
92
+ private def auth_headers = {}
93
+
94
+ # @api private
95
+ #
96
+ # @param req [Hash{Symbol=>Object}] .
97
+ #
98
+ # @option req [Symbol] :method
99
+ #
100
+ # @option req [String, Array<String>] :path
101
+ #
102
+ # @option req [Hash{String=>Array<String>, String, nil}, nil] :query
103
+ #
104
+ # @option req [Hash{String=>String, Integer, Array<String, Integer, nil>, nil}, nil] :headers
105
+ #
106
+ # @option req [Object, nil] :body
107
+ #
108
+ # @option req [Symbol, nil] :unwrap
109
+ #
110
+ # @option req [Class, nil] :page
111
+ #
112
+ # @option req [Class, nil] :stream
113
+ #
114
+ # @option req [Pangea::Internal::Type::Converter, Class, nil] :model
115
+ #
116
+ # @param opts [Hash{Symbol=>Object}] .
117
+ #
118
+ # @option opts [String, nil] :idempotency_key
119
+ #
120
+ # @option opts [Hash{String=>Array<String>, String, nil}, nil] :extra_query
121
+ #
122
+ # @option opts [Hash{String=>String, nil}, nil] :extra_headers
123
+ #
124
+ # @option opts [Object, nil] :extra_body
125
+ #
126
+ # @option opts [Integer, nil] :max_retries
127
+ #
128
+ # @option opts [Float, nil] :timeout
129
+ #
130
+ # @return [Hash{Symbol=>Object}]
131
+ private def build_request(req, opts)
132
+ method, uninterpolated_path = req.fetch_values(:method, :path)
133
+
134
+ path = Pangea::Internal::Util.interpolate_path(uninterpolated_path)
135
+
136
+ query = Pangea::Internal::Util.deep_merge(req[:query].to_h, opts[:extra_query].to_h)
137
+
138
+ headers = Pangea::Internal::Util.normalized_headers(
139
+ @headers,
140
+ auth_headers,
141
+ req[:headers].to_h,
142
+ opts[:extra_headers].to_h
143
+ )
144
+
145
+ if @idempotency_header &&
146
+ !headers.key?(@idempotency_header) &&
147
+ (!Net::HTTP::IDEMPOTENT_METHODS_.include?(method.to_s.upcase) || opts.key?(:idempotency_key))
148
+ headers[@idempotency_header] = opts.fetch(:idempotency_key) { generate_idempotency_key }
149
+ end
150
+
151
+ timeout = opts.fetch(:timeout, @timeout).to_f.clamp(0..)
152
+
153
+ headers.reject! { |_, v| v.to_s.empty? }
154
+
155
+ body =
156
+ case method
157
+ in :get | :head | :options | :trace
158
+ nil
159
+ else
160
+ Pangea::Internal::Util.deep_merge(*[req[:body], opts[:extra_body]].compact)
161
+ end
162
+
163
+ headers, encoded = Pangea::Internal::Util.encode_content(headers, body)
164
+ {
165
+ method: method,
166
+ url: Pangea::Internal::Util.join_parsed_uri(@base_url, {**req, path: path, query: query}),
167
+ headers: headers,
168
+ body: encoded,
169
+ max_retries: opts.fetch(:max_retries, @max_retries),
170
+ timeout: timeout
171
+ }
172
+ end
173
+
174
+ # @api private
175
+ #
176
+ # @param headers [Hash{String=>String}]
177
+ # @param retry_count [Integer]
178
+ #
179
+ # @return [Float]
180
+ private def retry_delay(headers, retry_count:)
181
+ span = Float(headers["retry-after-ms"], exception: false)&.then { _1 / 1000 }
182
+ return span if span
183
+
184
+ retry_header = headers["retry-after"]
185
+ return span if (span = Float(retry_header, exception: false))
186
+
187
+ span = retry_header&.then do
188
+ Time.httpdate(_1) - Time.now
189
+ rescue ArgumentError
190
+ nil
191
+ end
192
+ return span if span
193
+
194
+ scale = retry_count**2
195
+ jitter = 1 - (0.25 * rand)
196
+ (@initial_retry_delay * scale * jitter).clamp(0, @max_retry_delay)
197
+ end
198
+
199
+ # @api private
200
+ #
201
+ # @param request [Hash{Symbol=>Object}] .
202
+ #
203
+ # @option request [Symbol] :method
204
+ #
205
+ # @option request [URI::Generic] :url
206
+ #
207
+ # @option request [Hash{String=>String}] :headers
208
+ #
209
+ # @option request [Object] :body
210
+ #
211
+ # @option request [Integer] :max_retries
212
+ #
213
+ # @option request [Float] :timeout
214
+ #
215
+ # @param redirect_count [Integer]
216
+ #
217
+ # @param retry_count [Integer]
218
+ #
219
+ # @param send_retry_header [Boolean]
220
+ #
221
+ # @raise [Pangea::Errors::APIError]
222
+ # @return [Array(Integer, Net::HTTPResponse, Enumerable<String>)]
223
+ private def send_request(request, redirect_count:, retry_count:, send_retry_header:)
224
+ url, _headers, max_retries, timeout = request.fetch_values(:url, :headers, :max_retries, :timeout)
225
+ input = {**request.except(:timeout), deadline: Pangea::Internal::Util.monotonic_secs + timeout}
226
+
227
+ begin
228
+ status, response, stream = @requester.execute(input)
229
+ rescue Pangea::Errors::APIConnectionError => e
230
+ status = e
231
+ end
232
+
233
+ case status
234
+ in ..299
235
+ [status, response, stream]
236
+ in 300..399 if redirect_count >= self.class::MAX_REDIRECTS
237
+ self.class.reap_connection!(status, stream: stream)
238
+
239
+ message = "Failed to complete the request within #{self.class::MAX_REDIRECTS} redirects."
240
+ raise Pangea::Errors::APIConnectionError.new(url: url, response: response, message: message)
241
+ in 300..399
242
+ self.class.reap_connection!(status, stream: stream)
243
+
244
+ request = self.class.follow_redirect(request, status: status, response_headers: response)
245
+ send_request(
246
+ request,
247
+ redirect_count: redirect_count + 1,
248
+ retry_count: retry_count,
249
+ send_retry_header: send_retry_header
250
+ )
251
+ in Pangea::Errors::APIConnectionError if retry_count >= max_retries
252
+ raise status
253
+ in (400..) if retry_count >= max_retries || !self.class.should_retry?(status, headers: response)
254
+ decoded = Kernel.then do
255
+ Pangea::Internal::Util.decode_content(response, stream: stream, suppress_error: true)
256
+ ensure
257
+ self.class.reap_connection!(status, stream: stream)
258
+ end
259
+
260
+ raise Pangea::Errors::APIStatusError.for(
261
+ url: url,
262
+ status: status,
263
+ body: decoded,
264
+ request: nil,
265
+ response: response
266
+ )
267
+ in (400..) | Pangea::Errors::APIConnectionError
268
+ self.class.reap_connection!(status, stream: stream)
269
+
270
+ delay = retry_delay(response || {}, retry_count: retry_count)
271
+ sleep(delay)
272
+
273
+ send_request(
274
+ request,
275
+ redirect_count: redirect_count,
276
+ retry_count: retry_count + 1,
277
+ send_retry_header: send_retry_header
278
+ )
279
+ end
280
+ end
281
+
282
+ # Execute the request specified by `req`. This is the method that all resource
283
+ # methods call into.
284
+ #
285
+ # @overload request(method, path, query: {}, headers: {}, body: nil, unwrap: nil, page: nil, stream: nil, model: Pangea::Internal::Type::Unknown, options: {})
286
+ #
287
+ # @param method [Symbol]
288
+ #
289
+ # @param path [String, Array<String>]
290
+ #
291
+ # @param query [Hash{String=>Array<String>, String, nil}, nil]
292
+ #
293
+ # @param headers [Hash{String=>String, Integer, Array<String, Integer, nil>, nil}, nil]
294
+ #
295
+ # @param body [Object, nil]
296
+ #
297
+ # @param unwrap [Symbol, nil]
298
+ #
299
+ # @param page [Class, nil]
300
+ #
301
+ # @param model [Pangea::Internal::Type::Converter, Class, nil]
302
+ #
303
+ # @param options [Pangea::RequestOptions, Hash{Symbol=>Object}, nil] .
304
+ #
305
+ # @option options [Hash{String=>Array<String>, String, nil}, nil] :extra_query
306
+ #
307
+ # @option options [Hash{String=>String, nil}, nil] :extra_headers
308
+ #
309
+ # @option options [Object, nil] :extra_body
310
+ #
311
+ # @option options [Integer, nil] :max_retries
312
+ #
313
+ # @option options [Float, nil] :timeout
314
+ #
315
+ # @raise [Pangea::Errors::APIError]
316
+ # @return [Object]
317
+ def request(req)
318
+ self.class.validate!(req)
319
+ model = req.fetch(:model) { Pangea::Internal::Type::Unknown }
320
+ opts = req[:options].to_h
321
+ Pangea::RequestOptions.validate!(opts)
322
+ request = build_request(req.except(:options), opts)
323
+ _url = request.fetch(:url)
324
+
325
+ _status, response, stream = send_request(
326
+ request,
327
+ redirect_count: 0,
328
+ retry_count: 0,
329
+ send_retry_header: false
330
+ )
331
+
332
+ decoded = Pangea::Internal::Util.decode_content(response, stream: stream)
333
+ case req
334
+ in {structure: Class => pr}
335
+ pr.new(response_data: decoded, model: model)
336
+ in {page: Class => page}
337
+ page.new(client: self, req: req, headers: response, page_data: decoded)
338
+ else
339
+ unwrapped = Pangea::Internal::Util.dig(decoded, req[:unwrap])
340
+ Pangea::Internal::Type::Converter.coerce(model, unwrapped)
341
+ end
342
+ end
343
+ end
344
+ end
345
+ end
346
+ end