berrycrawl 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/.fern/metadata.json +12 -0
- data/.rubocop.yml +103 -0
- data/CONTRIBUTING.md +120 -0
- data/LICENSE +17 -0
- data/README.md +183 -0
- data/Rakefile +20 -0
- data/custom.gemspec.rb +16 -0
- data/lib/Berrycrawl/account/client.rb +44 -0
- data/lib/Berrycrawl/brand/client.rb +48 -0
- data/lib/Berrycrawl/brand/types/brand_dto.rb +15 -0
- data/lib/Berrycrawl/client.rb +268 -0
- data/lib/Berrycrawl/environment.rb +7 -0
- data/lib/Berrycrawl/errors/api_error.rb +8 -0
- data/lib/Berrycrawl/errors/client_error.rb +17 -0
- data/lib/Berrycrawl/errors/redirect_error.rb +8 -0
- data/lib/Berrycrawl/errors/response_error.rb +42 -0
- data/lib/Berrycrawl/errors/server_error.rb +11 -0
- data/lib/Berrycrawl/errors/timeout_error.rb +8 -0
- data/lib/Berrycrawl/internal/errors/constraint_error.rb +10 -0
- data/lib/Berrycrawl/internal/errors/type_error.rb +10 -0
- data/lib/Berrycrawl/internal/http/base_request.rb +51 -0
- data/lib/Berrycrawl/internal/http/raw_client.rb +252 -0
- data/lib/Berrycrawl/internal/iterators/cursor_item_iterator.rb +28 -0
- data/lib/Berrycrawl/internal/iterators/cursor_page_iterator.rb +63 -0
- data/lib/Berrycrawl/internal/iterators/item_iterator.rb +65 -0
- data/lib/Berrycrawl/internal/iterators/offset_item_iterator.rb +30 -0
- data/lib/Berrycrawl/internal/iterators/offset_page_iterator.rb +103 -0
- data/lib/Berrycrawl/internal/json/request.rb +41 -0
- data/lib/Berrycrawl/internal/json/serializable.rb +25 -0
- data/lib/Berrycrawl/internal/multipart/multipart_encoder.rb +141 -0
- data/lib/Berrycrawl/internal/multipart/multipart_form_data.rb +78 -0
- data/lib/Berrycrawl/internal/multipart/multipart_form_data_part.rb +51 -0
- data/lib/Berrycrawl/internal/multipart/multipart_request.rb +40 -0
- data/lib/Berrycrawl/internal/types/array.rb +47 -0
- data/lib/Berrycrawl/internal/types/boolean.rb +34 -0
- data/lib/Berrycrawl/internal/types/enum.rb +56 -0
- data/lib/Berrycrawl/internal/types/hash.rb +36 -0
- data/lib/Berrycrawl/internal/types/model/field.rb +38 -0
- data/lib/Berrycrawl/internal/types/model.rb +208 -0
- data/lib/Berrycrawl/internal/types/type.rb +35 -0
- data/lib/Berrycrawl/internal/types/union.rb +161 -0
- data/lib/Berrycrawl/internal/types/unknown.rb +15 -0
- data/lib/Berrycrawl/internal/types/utils.rb +116 -0
- data/lib/Berrycrawl/jobs/client.rb +127 -0
- data/lib/Berrycrawl/jobs/types/cancel_jobs_request.rb +11 -0
- data/lib/Berrycrawl/jobs/types/get_jobs_request.rb +15 -0
- data/lib/Berrycrawl/jobs/types/list_jobs_request.rb +17 -0
- data/lib/Berrycrawl/jobs/types/list_jobs_request_status.rb +17 -0
- data/lib/Berrycrawl/jobs/types/list_jobs_request_type.rb +14 -0
- data/lib/Berrycrawl/types/account_response.rb +11 -0
- data/lib/Berrycrawl/types/account_response_data.rb +21 -0
- data/lib/Berrycrawl/types/account_response_data_plan.rb +17 -0
- data/lib/Berrycrawl/types/account_response_data_queue.rb +11 -0
- data/lib/Berrycrawl/types/action_dto.rb +21 -0
- data/lib/Berrycrawl/types/action_dto_direction.rb +12 -0
- data/lib/Berrycrawl/types/action_dto_type.rb +18 -0
- data/lib/Berrycrawl/types/agent_config_dto.rb +11 -0
- data/lib/Berrycrawl/types/agent_config_dto_mode.rb +12 -0
- data/lib/Berrycrawl/types/brand_asset.rb +17 -0
- data/lib/Berrycrawl/types/brand_profile.rb +27 -0
- data/lib/Berrycrawl/types/brand_profile_colors_item.rb +11 -0
- data/lib/Berrycrawl/types/brand_profile_fonts_item.rb +11 -0
- data/lib/Berrycrawl/types/brand_profile_socials_item.rb +11 -0
- data/lib/Berrycrawl/types/brand_response.rb +11 -0
- data/lib/Berrycrawl/types/brand_response_data.rb +11 -0
- data/lib/Berrycrawl/types/brand_response_data_meta.rb +15 -0
- data/lib/Berrycrawl/types/cancel_job_response.rb +11 -0
- data/lib/Berrycrawl/types/cancel_job_response_data.rb +11 -0
- data/lib/Berrycrawl/types/change_tracking_format_dto.rb +15 -0
- data/lib/Berrycrawl/types/change_tracking_format_dto_type.rb +11 -0
- data/lib/Berrycrawl/types/crawl_dto.rb +41 -0
- data/lib/Berrycrawl/types/crawl_dto_sitemap.rb +13 -0
- data/lib/Berrycrawl/types/extract_dto.rb +29 -0
- data/lib/Berrycrawl/types/extract_webhook_config_dto.rb +15 -0
- data/lib/Berrycrawl/types/job_created_response.rb +15 -0
- data/lib/Berrycrawl/types/job_response.rb +11 -0
- data/lib/Berrycrawl/types/job_response_data.rb +37 -0
- data/lib/Berrycrawl/types/job_response_data_errors_item.rb +11 -0
- data/lib/Berrycrawl/types/job_summary.rb +29 -0
- data/lib/Berrycrawl/types/job_summary_type.rb +12 -0
- data/lib/Berrycrawl/types/json_format_dto.rb +13 -0
- data/lib/Berrycrawl/types/json_format_dto_type.rb +11 -0
- data/lib/Berrycrawl/types/list_jobs_response.rb +11 -0
- data/lib/Berrycrawl/types/list_jobs_response_data.rb +11 -0
- data/lib/Berrycrawl/types/list_webhooks_response.rb +11 -0
- data/lib/Berrycrawl/types/location_dto.rb +11 -0
- data/lib/Berrycrawl/types/map_dto.rb +23 -0
- data/lib/Berrycrawl/types/map_dto_sitemap.rb +13 -0
- data/lib/Berrycrawl/types/map_link.rb +13 -0
- data/lib/Berrycrawl/types/map_response.rb +15 -0
- data/lib/Berrycrawl/types/message_response.rb +11 -0
- data/lib/Berrycrawl/types/pagination.rb +15 -0
- data/lib/Berrycrawl/types/parse_dto.rb +11 -0
- data/lib/Berrycrawl/types/scrape_dto.rb +49 -0
- data/lib/Berrycrawl/types/scrape_dto_formats_item.rb +17 -0
- data/lib/Berrycrawl/types/scrape_dto_formats_item_zero.rb +18 -0
- data/lib/Berrycrawl/types/scrape_dto_proxy.rb +16 -0
- data/lib/Berrycrawl/types/scrape_metadata.rb +17 -0
- data/lib/Berrycrawl/types/scrape_response.rb +17 -0
- data/lib/Berrycrawl/types/scrape_response_credits.rb +9 -0
- data/lib/Berrycrawl/types/screenshot_clip_dto.rb +15 -0
- data/lib/Berrycrawl/types/screenshot_cookie_dto.rb +21 -0
- data/lib/Berrycrawl/types/screenshot_cookie_dto_same_site.rb +13 -0
- data/lib/Berrycrawl/types/screenshot_dto.rb +79 -0
- data/lib/Berrycrawl/types/screenshot_dto_color_scheme.rb +12 -0
- data/lib/Berrycrawl/types/screenshot_dto_device.rb +15 -0
- data/lib/Berrycrawl/types/screenshot_dto_format.rb +13 -0
- data/lib/Berrycrawl/types/screenshot_dto_full_page_algorithm.rb +13 -0
- data/lib/Berrycrawl/types/screenshot_dto_proxy.rb +16 -0
- data/lib/Berrycrawl/types/screenshot_dto_response_format.rb +12 -0
- data/lib/Berrycrawl/types/screenshot_dto_scale.rb +12 -0
- data/lib/Berrycrawl/types/screenshot_dto_wait_until.rb +13 -0
- data/lib/Berrycrawl/types/screenshot_location_dto.rb +11 -0
- data/lib/Berrycrawl/types/screenshot_viewport_dto.rb +11 -0
- data/lib/Berrycrawl/types/search_dto.rb +23 -0
- data/lib/Berrycrawl/types/search_response.rb +19 -0
- data/lib/Berrycrawl/types/search_response_provider.rb +11 -0
- data/lib/Berrycrawl/types/search_result.rb +19 -0
- data/lib/Berrycrawl/types/search_result_provider.rb +11 -0
- data/lib/Berrycrawl/types/summary_format_dto.rb +11 -0
- data/lib/Berrycrawl/types/summary_format_dto_type.rb +11 -0
- data/lib/Berrycrawl/types/test_webhook_response.rb +15 -0
- data/lib/Berrycrawl/types/webhook.rb +23 -0
- data/lib/Berrycrawl/types/webhook_config_dto.rb +15 -0
- data/lib/Berrycrawl/types/webhook_response.rb +11 -0
- data/lib/Berrycrawl/version.rb +5 -0
- data/lib/Berrycrawl/webhooks/client.rb +214 -0
- data/lib/Berrycrawl/webhooks/types/create_webhook_dto.rb +13 -0
- data/lib/Berrycrawl/webhooks/types/delete_webhooks_request.rb +11 -0
- data/lib/Berrycrawl/webhooks/types/get_webhooks_request.rb +11 -0
- data/lib/Berrycrawl/webhooks/types/test_webhook_dto.rb +15 -0
- data/lib/Berrycrawl/webhooks/types/update_webhook_dto.rb +17 -0
- data/lib/Berrycrawl.rb +130 -0
- data/reference.md +1759 -0
- metadata +179 -0
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Berrycrawl
|
|
4
|
+
module Internal
|
|
5
|
+
module Http
|
|
6
|
+
# @api private
|
|
7
|
+
class RawClient
|
|
8
|
+
# Default HTTP status codes that trigger a retry
|
|
9
|
+
RETRYABLE_STATUSES = [408, 429, 500, 502, 503, 504, 521, 522, 524].freeze
|
|
10
|
+
# Initial delay between retries in seconds
|
|
11
|
+
INITIAL_RETRY_DELAY = 0.5
|
|
12
|
+
# Maximum delay between retries in seconds
|
|
13
|
+
MAX_RETRY_DELAY = 60.0
|
|
14
|
+
# Jitter factor for randomizing retry delays (20%)
|
|
15
|
+
JITTER_FACTOR = 0.2
|
|
16
|
+
|
|
17
|
+
# @return [String] The base URL for requests
|
|
18
|
+
attr_reader :base_url
|
|
19
|
+
|
|
20
|
+
# @param base_url [String] The base url for the request.
|
|
21
|
+
# @param max_retries [Integer] The number of times to retry a failed request, defaults to 2.
|
|
22
|
+
# @param timeout [Float] The timeout for the request, defaults to 60.0 seconds.
|
|
23
|
+
# @param headers [Hash] The headers for the request.
|
|
24
|
+
# @param auth_provider [Object, nil] An optional auth provider responding to
|
|
25
|
+
# `auth_headers`. When present its headers are resolved on every request so
|
|
26
|
+
# token-based schemes (e.g. OAuth) can refresh an expired token mid-session.
|
|
27
|
+
def initialize(base_url:, max_retries: 2, timeout: 60.0, headers: {}, auth_provider: nil)
|
|
28
|
+
@base_url = base_url
|
|
29
|
+
@max_retries = max_retries
|
|
30
|
+
@timeout = timeout
|
|
31
|
+
@auth_provider = auth_provider
|
|
32
|
+
@default_headers = {
|
|
33
|
+
"X-Fern-Language": "Ruby",
|
|
34
|
+
"X-Fern-SDK-Name": "berrycrawl",
|
|
35
|
+
"X-Fern-SDK-Version": "0.0.1"
|
|
36
|
+
}.merge(headers)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# @param request [Berrycrawl::Internal::Http::BaseRequest] The HTTP request.
|
|
40
|
+
# @return [HTTP::Response] The HTTP response.
|
|
41
|
+
def send(request)
|
|
42
|
+
url = build_url(request)
|
|
43
|
+
# Resolve auth headers once per request (not per retry) so token-based
|
|
44
|
+
# providers refresh at most once here; static providers are cheap.
|
|
45
|
+
auth_headers = resolve_auth_headers
|
|
46
|
+
attempt = 0
|
|
47
|
+
response = nil
|
|
48
|
+
|
|
49
|
+
loop do
|
|
50
|
+
http_request = build_http_request(
|
|
51
|
+
url:,
|
|
52
|
+
method: request.method,
|
|
53
|
+
headers: request.encode_headers(protected_keys: @default_headers.keys + auth_headers.keys),
|
|
54
|
+
body: request.encode_body,
|
|
55
|
+
auth_headers: auth_headers
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
conn = connect(url)
|
|
59
|
+
conn.open_timeout = @timeout
|
|
60
|
+
conn.read_timeout = @timeout
|
|
61
|
+
conn.write_timeout = @timeout
|
|
62
|
+
conn.continue_timeout = @timeout
|
|
63
|
+
|
|
64
|
+
response = conn.request(http_request)
|
|
65
|
+
|
|
66
|
+
break unless should_retry?(response, attempt)
|
|
67
|
+
|
|
68
|
+
delay = retry_delay(response, attempt)
|
|
69
|
+
sleep(delay)
|
|
70
|
+
attempt += 1
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
response
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Determines if a request should be retried based on the response status code.
|
|
77
|
+
# @param response [Net::HTTPResponse] The HTTP response.
|
|
78
|
+
# @param attempt [Integer] The current retry attempt (0-indexed).
|
|
79
|
+
# @return [Boolean] Whether the request should be retried.
|
|
80
|
+
def should_retry?(response, attempt)
|
|
81
|
+
return false if attempt >= @max_retries
|
|
82
|
+
|
|
83
|
+
status = response.code.to_i
|
|
84
|
+
RETRYABLE_STATUSES.include?(status)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Calculates the delay before the next retry attempt using exponential backoff with jitter.
|
|
88
|
+
# Respects Retry-After header if present.
|
|
89
|
+
# @param response [Net::HTTPResponse] The HTTP response.
|
|
90
|
+
# @param attempt [Integer] The current retry attempt (0-indexed).
|
|
91
|
+
# @return [Float] The delay in seconds before the next retry.
|
|
92
|
+
def retry_delay(response, attempt)
|
|
93
|
+
# Check for Retry-After header (can be seconds or HTTP date)
|
|
94
|
+
retry_after = response["Retry-After"]
|
|
95
|
+
if retry_after
|
|
96
|
+
delay = parse_retry_after(retry_after)
|
|
97
|
+
return [delay, MAX_RETRY_DELAY].min if delay&.positive?
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Exponential backoff with jitter: base_delay * 2^attempt
|
|
101
|
+
base_delay = INITIAL_RETRY_DELAY * (2**attempt)
|
|
102
|
+
add_jitter([base_delay, MAX_RETRY_DELAY].min)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Parses the Retry-After header value.
|
|
106
|
+
# @param value [String] The Retry-After header value (seconds or HTTP date).
|
|
107
|
+
# @return [Float, nil] The delay in seconds, or nil if parsing fails.
|
|
108
|
+
def parse_retry_after(value)
|
|
109
|
+
# Try parsing as integer (seconds)
|
|
110
|
+
seconds = Integer(value, exception: false)
|
|
111
|
+
return seconds.to_f if seconds
|
|
112
|
+
|
|
113
|
+
# Try parsing as HTTP date
|
|
114
|
+
begin
|
|
115
|
+
retry_time = Time.httpdate(value)
|
|
116
|
+
delay = retry_time - Time.now
|
|
117
|
+
delay.positive? ? delay : nil
|
|
118
|
+
rescue ArgumentError
|
|
119
|
+
nil
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Adds random jitter to a delay value.
|
|
124
|
+
# @param delay [Float] The base delay in seconds.
|
|
125
|
+
# @return [Float] The delay with jitter applied.
|
|
126
|
+
def add_jitter(delay)
|
|
127
|
+
jitter = delay * JITTER_FACTOR * (rand - 0.5) * 2
|
|
128
|
+
[delay + jitter, 0].max
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
LOCALHOST_HOSTS = %w[localhost 127.0.0.1 [::1]].freeze
|
|
132
|
+
|
|
133
|
+
# @param request [Berrycrawl::Internal::Http::BaseRequest] The HTTP request.
|
|
134
|
+
# @return [URI::Generic] The URL.
|
|
135
|
+
def build_url(request)
|
|
136
|
+
encoded_query = request.encode_query
|
|
137
|
+
|
|
138
|
+
# If the path is already an absolute URL, use it directly
|
|
139
|
+
if request.path.start_with?("http://", "https://")
|
|
140
|
+
url = request.path
|
|
141
|
+
url = "#{url}?#{encode_query(encoded_query)}" if encoded_query&.any?
|
|
142
|
+
parsed = URI.parse(url)
|
|
143
|
+
validate_https!(parsed)
|
|
144
|
+
return parsed
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
path = request.path.start_with?("/") ? request.path[1..] : request.path
|
|
148
|
+
base = request.base_url || @base_url
|
|
149
|
+
url = "#{base.chomp("/")}/#{path}"
|
|
150
|
+
url = "#{url}?#{encode_query(encoded_query)}" if encoded_query&.any?
|
|
151
|
+
parsed = URI.parse(url)
|
|
152
|
+
validate_https!(parsed)
|
|
153
|
+
parsed
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Raises if the URL uses http:// for a non-localhost host, which would
|
|
157
|
+
# send authentication credentials in plaintext.
|
|
158
|
+
# @param url [URI::Generic] The parsed URL.
|
|
159
|
+
def validate_https!(url)
|
|
160
|
+
return if url.scheme != "http"
|
|
161
|
+
return if LOCALHOST_HOSTS.include?(url.host)
|
|
162
|
+
|
|
163
|
+
raise ArgumentError,
|
|
164
|
+
"Refusing to send request to non-HTTPS URL: #{url}. " \
|
|
165
|
+
"HTTP is only allowed for localhost. Use HTTPS or pass a localhost URL."
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Resolves the auth headers to send with the next request. Delegates to the
|
|
169
|
+
# configured auth provider (if any) on every call so that token-based
|
|
170
|
+
# providers (e.g. OAuth client-credentials) can refresh an expired token
|
|
171
|
+
# before the request is sent. Returns an empty hash when no provider is set,
|
|
172
|
+
# which keeps the api-key / basic / bearer / no-auth paths unchanged.
|
|
173
|
+
# @return [Hash] The auth headers for the current request.
|
|
174
|
+
def resolve_auth_headers
|
|
175
|
+
return {} if @auth_provider.nil?
|
|
176
|
+
|
|
177
|
+
@auth_provider.auth_headers
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# @param url [URI::Generic] The url to the resource.
|
|
181
|
+
# @param method [String] The HTTP method to use.
|
|
182
|
+
# @param headers [Hash] The headers for the request.
|
|
183
|
+
# @param body [String, nil] The body for the request.
|
|
184
|
+
# @param auth_headers [Hash] The auth headers resolved for this request. These
|
|
185
|
+
# take precedence over the static default headers but not over per-request
|
|
186
|
+
# headers, mirroring the previous baked-header precedence.
|
|
187
|
+
# @return [HTTP::Request] The HTTP request.
|
|
188
|
+
def build_http_request(url:, method:, headers: {}, body: nil, auth_headers: {})
|
|
189
|
+
request = Net::HTTPGenericRequest.new(
|
|
190
|
+
method,
|
|
191
|
+
!body.nil?,
|
|
192
|
+
method != "HEAD",
|
|
193
|
+
url
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
request_headers = @default_headers.merge(auth_headers).merge(headers)
|
|
197
|
+
request_headers.each { |name, value| request[name] = value }
|
|
198
|
+
request.body = body if body
|
|
199
|
+
|
|
200
|
+
# Net::HTTP disables its transparent gzip/deflate decoding as soon as an
|
|
201
|
+
# Accept-Encoding header is set explicitly on the request. Re-enable it so
|
|
202
|
+
# that compressed response bodies are still inflated.
|
|
203
|
+
request.extend(DecodeContent) if request_headers.keys.any? { |name| name.to_s.casecmp("accept-encoding").zero? }
|
|
204
|
+
|
|
205
|
+
request
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# Keeps Net::HTTP's transparent gzip/deflate response decoding enabled
|
|
209
|
+
# even when an Accept-Encoding header is set explicitly on the request.
|
|
210
|
+
# @api private
|
|
211
|
+
module DecodeContent
|
|
212
|
+
def decode_content # rubocop:disable Naming/PredicateMethod
|
|
213
|
+
true
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# @param query [Hash] The query for the request.
|
|
218
|
+
# @return [String, nil] The encoded query.
|
|
219
|
+
def encode_query(query)
|
|
220
|
+
query.to_h.empty? ? nil : URI.encode_www_form(query)
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
# @param url [URI::Generic] The url to connect to.
|
|
224
|
+
# @return [Net::HTTP] The HTTP connection.
|
|
225
|
+
def connect(url)
|
|
226
|
+
is_https = (url.scheme == "https")
|
|
227
|
+
|
|
228
|
+
port = if url.port
|
|
229
|
+
url.port
|
|
230
|
+
elsif is_https
|
|
231
|
+
Net::HTTP.https_default_port
|
|
232
|
+
else
|
|
233
|
+
Net::HTTP.http_default_port
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
http = Net::HTTP.new(url.host, port)
|
|
237
|
+
http.use_ssl = is_https
|
|
238
|
+
http.verify_mode = OpenSSL::SSL::VERIFY_PEER if is_https
|
|
239
|
+
# NOTE: We handle retries at the application level with HTTP status code awareness,
|
|
240
|
+
# so we set max_retries to 0 to disable Net::HTTP's built-in network-level retries.
|
|
241
|
+
http.max_retries = 0
|
|
242
|
+
http
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
# @return [String]
|
|
246
|
+
def inspect
|
|
247
|
+
"#<#{self.class.name}:0x#{object_id.to_s(16)} @base_url=#{@base_url.inspect}>"
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Berrycrawl
|
|
4
|
+
module Internal
|
|
5
|
+
class CursorItemIterator < ItemIterator
|
|
6
|
+
# Instantiates a CursorItemIterator, an Enumerable class which wraps calls to a cursor-based paginated API and yields individual items from it.
|
|
7
|
+
#
|
|
8
|
+
# @param initial_cursor [String] The initial cursor to use when iterating, if any.
|
|
9
|
+
# @param cursor_field [Symbol] The field in API responses to extract the next cursor from.
|
|
10
|
+
# @param item_field [Symbol] The field in API responses to extract the items to iterate over.
|
|
11
|
+
# @param block [Proc] A block which is responsible for receiving a cursor to use and returning the given page from the API.
|
|
12
|
+
# @return [Berrycrawl::Internal::CursorItemIterator]
|
|
13
|
+
def initialize(initial_cursor:, cursor_field:, item_field:, &)
|
|
14
|
+
super()
|
|
15
|
+
@item_field = item_field
|
|
16
|
+
@page_iterator = CursorPageIterator.new(initial_cursor:, cursor_field:, &)
|
|
17
|
+
@page = nil
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Returns the CursorPageIterator mediating access to the underlying API.
|
|
21
|
+
#
|
|
22
|
+
# @return [Berrycrawl::Internal::CursorPageIterator]
|
|
23
|
+
def pages
|
|
24
|
+
@page_iterator
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Berrycrawl
|
|
4
|
+
module Internal
|
|
5
|
+
class CursorPageIterator
|
|
6
|
+
include Enumerable
|
|
7
|
+
|
|
8
|
+
# The raw HTTP response from the most recent page response.
|
|
9
|
+
# @return [Net::HTTPResponse, nil]
|
|
10
|
+
attr_reader :http_response
|
|
11
|
+
|
|
12
|
+
# Instantiates a CursorPageIterator, an Enumerable class which wraps calls to a cursor-based paginated API and yields pages of items.
|
|
13
|
+
#
|
|
14
|
+
# @param initial_cursor [String] The initial cursor to use when iterating, if any.
|
|
15
|
+
# @param cursor_field [Symbol] The name of the field in API responses to extract the next cursor from.
|
|
16
|
+
# @param block [Proc] A block which is responsible for receiving a cursor to use and returning the given page from the API.
|
|
17
|
+
# The block should return a two-element array: [parsed_page, raw_http_response].
|
|
18
|
+
# @return [Berrycrawl::Internal::CursorPageIterator]
|
|
19
|
+
def initialize(initial_cursor:, cursor_field:, &block)
|
|
20
|
+
@need_initial_load = initial_cursor.nil?
|
|
21
|
+
@cursor = initial_cursor
|
|
22
|
+
@cursor_field = cursor_field
|
|
23
|
+
@get_next_page = block
|
|
24
|
+
@http_response = nil
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Iterates over each page returned by the API.
|
|
28
|
+
#
|
|
29
|
+
# @param block [Proc] The block which each retrieved page is yielded to.
|
|
30
|
+
# @return [NilClass]
|
|
31
|
+
def each(&block)
|
|
32
|
+
while (page = next_page)
|
|
33
|
+
block.call(page)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Whether another page will be available from the API.
|
|
38
|
+
#
|
|
39
|
+
# @return [Boolean]
|
|
40
|
+
def next?
|
|
41
|
+
@need_initial_load || !@cursor.nil?
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Retrieves the next page from the API.
|
|
45
|
+
#
|
|
46
|
+
# @return [Object, nil]
|
|
47
|
+
def next_page
|
|
48
|
+
return if !@need_initial_load && @cursor.nil?
|
|
49
|
+
|
|
50
|
+
@need_initial_load = false
|
|
51
|
+
result = @get_next_page.call(@cursor)
|
|
52
|
+
if result.is_a?(Array)
|
|
53
|
+
fetched_page, raw_response = result
|
|
54
|
+
@http_response = raw_response
|
|
55
|
+
else
|
|
56
|
+
fetched_page = result
|
|
57
|
+
end
|
|
58
|
+
@cursor = fetched_page.send(@cursor_field)
|
|
59
|
+
fetched_page
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Berrycrawl
|
|
4
|
+
module Internal
|
|
5
|
+
class ItemIterator
|
|
6
|
+
include Enumerable
|
|
7
|
+
|
|
8
|
+
# The raw HTTP response from the most recent page response.
|
|
9
|
+
# @return [Net::HTTPResponse, nil]
|
|
10
|
+
def http_response
|
|
11
|
+
@page_iterator&.http_response
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Iterates over each item returned by the API.
|
|
15
|
+
#
|
|
16
|
+
# @param block [Proc] The block which each retrieved item is yielded to.
|
|
17
|
+
# @return [NilClass]
|
|
18
|
+
def each(&block)
|
|
19
|
+
while (item = next_element)
|
|
20
|
+
block.call(item)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Whether another item will be available from the API.
|
|
25
|
+
#
|
|
26
|
+
# @return [Boolean]
|
|
27
|
+
def next?
|
|
28
|
+
load_next_page if @page.nil?
|
|
29
|
+
return false if @page.nil?
|
|
30
|
+
|
|
31
|
+
return true if any_items_in_cached_page?
|
|
32
|
+
|
|
33
|
+
load_next_page
|
|
34
|
+
any_items_in_cached_page?
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Retrieves the next item from the API.
|
|
38
|
+
def next_element
|
|
39
|
+
item = next_item_from_cached_page
|
|
40
|
+
return item if item
|
|
41
|
+
|
|
42
|
+
load_next_page
|
|
43
|
+
next_item_from_cached_page
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
def next_item_from_cached_page
|
|
49
|
+
return unless @page
|
|
50
|
+
|
|
51
|
+
@page.send(@item_field).shift
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def any_items_in_cached_page?
|
|
55
|
+
return false unless @page
|
|
56
|
+
|
|
57
|
+
!@page.send(@item_field).empty?
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def load_next_page
|
|
61
|
+
@page = @page_iterator.next_page
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Berrycrawl
|
|
4
|
+
module Internal
|
|
5
|
+
class OffsetItemIterator < ItemIterator
|
|
6
|
+
# Instantiates an OffsetItemIterator, an Enumerable class which wraps calls to an offset-based paginated API and yields the individual items from it.
|
|
7
|
+
#
|
|
8
|
+
# @param initial_page [Integer] The initial page or offset to start from when iterating.
|
|
9
|
+
# @param item_field [Symbol] The name of the field in API responses to extract the items to iterate over.
|
|
10
|
+
# @param has_next_field [Symbol] The name of the field in API responses containing a boolean of whether another page exists.
|
|
11
|
+
# @param step [Boolean] If true, treats the page number as a true offset (i.e. increments the page number by the number of items returned from each call rather than just 1)
|
|
12
|
+
# @param block [Proc] A block which is responsible for receiving a page number to use and returning the given page from the API.
|
|
13
|
+
#
|
|
14
|
+
# @return [Berrycrawl::Internal::OffsetItemIterator]
|
|
15
|
+
def initialize(initial_page:, item_field:, has_next_field:, step:, &)
|
|
16
|
+
super()
|
|
17
|
+
@item_field = item_field
|
|
18
|
+
@page_iterator = OffsetPageIterator.new(initial_page:, item_field:, has_next_field:, step:, &)
|
|
19
|
+
@page = nil
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Returns the OffsetPageIterator that is mediating access to the underlying API.
|
|
23
|
+
#
|
|
24
|
+
# @return [Berrycrawl::Internal::OffsetPageIterator]
|
|
25
|
+
def pages
|
|
26
|
+
@page_iterator
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Berrycrawl
|
|
4
|
+
module Internal
|
|
5
|
+
class OffsetPageIterator
|
|
6
|
+
include Enumerable
|
|
7
|
+
|
|
8
|
+
# The raw HTTP response from the most recent page response.
|
|
9
|
+
# @return [Net::HTTPResponse, nil]
|
|
10
|
+
attr_reader :http_response
|
|
11
|
+
|
|
12
|
+
# Instantiates an OffsetPageIterator, an Enumerable class which wraps calls to an offset-based paginated API and yields pages of items from it.
|
|
13
|
+
#
|
|
14
|
+
# @param initial_page [Integer] The initial page to use when iterating, if any.
|
|
15
|
+
# @param item_field [Symbol] The field to pull the list of items to iterate over.
|
|
16
|
+
# @param has_next_field [Symbol] The field to pull the boolean of whether a next page exists from, if any.
|
|
17
|
+
# @param step [Boolean] If true, treats the page number as a true offset (i.e. increments the page number by the number of items returned from each call rather than just 1)
|
|
18
|
+
# @param block [Proc] A block which is responsible for receiving a page number to use and returning the given page from the API.
|
|
19
|
+
# The block should return a two-element array: [parsed_page, raw_http_response].
|
|
20
|
+
# @return [Berrycrawl::Internal::OffsetPageIterator]
|
|
21
|
+
def initialize(initial_page:, item_field:, has_next_field:, step:, &block)
|
|
22
|
+
@page_number = initial_page || (step ? 0 : 1)
|
|
23
|
+
@item_field = item_field
|
|
24
|
+
@has_next_field = has_next_field
|
|
25
|
+
@step = step
|
|
26
|
+
@get_next_page = block
|
|
27
|
+
|
|
28
|
+
# A cache of whether the API has another page, if it gives us that information...
|
|
29
|
+
@next_page = nil
|
|
30
|
+
# ...or the actual next page, preloaded, if it doesn't.
|
|
31
|
+
@has_next_page = nil
|
|
32
|
+
|
|
33
|
+
@http_response = nil
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Iterates over each page returned by the API.
|
|
37
|
+
#
|
|
38
|
+
# @param block [Proc] The block which each retrieved page is yielded to.
|
|
39
|
+
# @return [NilClass]
|
|
40
|
+
def each(&block)
|
|
41
|
+
while (page = next_page)
|
|
42
|
+
block.call(page)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Whether another page will be available from the API.
|
|
47
|
+
#
|
|
48
|
+
# @return [Boolean]
|
|
49
|
+
def next?
|
|
50
|
+
return @has_next_page unless @has_next_page.nil?
|
|
51
|
+
return true if @next_page
|
|
52
|
+
|
|
53
|
+
fetched_page = fetch_page(@page_number)
|
|
54
|
+
fetched_page_items = fetched_page&.send(@item_field)
|
|
55
|
+
if fetched_page_items.nil? || fetched_page_items.empty?
|
|
56
|
+
@has_next_page = false
|
|
57
|
+
else
|
|
58
|
+
@next_page = fetched_page
|
|
59
|
+
true
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Returns the next page from the API.
|
|
64
|
+
def next_page
|
|
65
|
+
return nil if @page_number.nil?
|
|
66
|
+
|
|
67
|
+
if @next_page
|
|
68
|
+
this_page = @next_page
|
|
69
|
+
@next_page = nil
|
|
70
|
+
else
|
|
71
|
+
this_page = fetch_page(@page_number)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
@has_next_page = this_page&.send(@has_next_field) if @has_next_field
|
|
75
|
+
|
|
76
|
+
items = this_page.send(@item_field)
|
|
77
|
+
if items.nil? || items.empty?
|
|
78
|
+
@page_number = nil
|
|
79
|
+
return nil
|
|
80
|
+
elsif @step
|
|
81
|
+
@page_number += items.length
|
|
82
|
+
else
|
|
83
|
+
@page_number += 1
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
this_page
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
private
|
|
90
|
+
|
|
91
|
+
def fetch_page(page_number)
|
|
92
|
+
result = @get_next_page.call(page_number)
|
|
93
|
+
if result.is_a?(Array)
|
|
94
|
+
fetched_page, raw_response = result
|
|
95
|
+
@http_response = raw_response
|
|
96
|
+
fetched_page
|
|
97
|
+
else
|
|
98
|
+
result
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Berrycrawl
|
|
4
|
+
module Internal
|
|
5
|
+
module JSON
|
|
6
|
+
# @api private
|
|
7
|
+
class Request < Berrycrawl::Internal::Http::BaseRequest
|
|
8
|
+
attr_reader :body
|
|
9
|
+
|
|
10
|
+
# @param base_url [String] The base URL for the request
|
|
11
|
+
# @param path [String] The path for the request
|
|
12
|
+
# @param method [Symbol] The HTTP method for the request (:get, :post, etc.)
|
|
13
|
+
# @param headers [Hash] Additional headers for the request (optional)
|
|
14
|
+
# @param query [Hash] Query parameters for the request (optional)
|
|
15
|
+
# @param body [Object, nil] The JSON request body (optional)
|
|
16
|
+
# @param request_options [Berrycrawl::RequestOptions, Hash{Symbol=>Object}, nil]
|
|
17
|
+
def initialize(base_url:, path:, method:, headers: {}, query: {}, body: nil, request_options: {})
|
|
18
|
+
super(base_url:, path:, method:, headers:, query:, request_options:)
|
|
19
|
+
|
|
20
|
+
@body = body
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# @return [Hash] The encoded HTTP request headers.
|
|
24
|
+
# @param protected_keys [Array<String>] Header keys set by the SDK client (e.g. auth, metadata)
|
|
25
|
+
# that must not be overridden by additional_headers from request_options.
|
|
26
|
+
def encode_headers(protected_keys: [])
|
|
27
|
+
sdk_headers = {
|
|
28
|
+
"Content-Type" => "application/json",
|
|
29
|
+
"Accept" => "application/json"
|
|
30
|
+
}.merge(@headers)
|
|
31
|
+
merge_additional_headers(sdk_headers, protected_keys:)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# @return [String, nil] The encoded HTTP request body.
|
|
35
|
+
def encode_body
|
|
36
|
+
@body.nil? ? nil : ::JSON.generate(@body)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Berrycrawl
|
|
4
|
+
module Internal
|
|
5
|
+
module JSON
|
|
6
|
+
module Serializable
|
|
7
|
+
# Loads data from JSON into its deserialized form
|
|
8
|
+
#
|
|
9
|
+
# @param str [String] Raw JSON to load into an object
|
|
10
|
+
# @return [Object]
|
|
11
|
+
def load(str)
|
|
12
|
+
raise NotImplementedError
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Dumps data from its deserialized form into JSON
|
|
16
|
+
#
|
|
17
|
+
# @param value [Object] The deserialized value
|
|
18
|
+
# @return [String]
|
|
19
|
+
def dump(value)
|
|
20
|
+
raise NotImplementedError
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|