fetch_util 0.3.1 → 0.4.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.
@@ -38,11 +38,11 @@ module FetchUtil
38
38
  work = Array(urls).compact.map(&:to_s).reject(&:empty?)
39
39
  return [] if work.empty?
40
40
 
41
- jobs = Queue.new
42
- failures = Queue.new
43
- work.each_with_index { |url, index| jobs << [index, url] }
44
41
  results = Array.new(work.length)
45
42
  worker_count = [@concurrency, work.length].min
43
+ failures = []
44
+ next_index = 0
45
+ mutex = Mutex.new
46
46
 
47
47
  threads = Array.new(worker_count) do
48
48
  Thread.new do
@@ -50,44 +50,39 @@ module FetchUtil
50
50
 
51
51
  begin
52
52
  loop do
53
- begin
54
- index, url = jobs.pop(true)
55
- rescue ThreadError
56
- break
53
+ index = mutex.synchronize do
54
+ if next_index < work.length
55
+ current = next_index
56
+ next_index += 1
57
+ current
58
+ end
57
59
  end
60
+ break if index.nil?
61
+
62
+ url = work[index]
58
63
 
59
64
  begin
60
65
  results[index] = fetcher.fetch(url)
61
66
  rescue StandardError => e
62
- failures << Failure.new(index: index, url: url, error: e)
67
+ mutex.synchronize { failures << Failure.new(index: index, url: url, error: e) }
63
68
  end
64
69
  end
65
70
  ensure
66
71
  fetcher.quit if fetcher.respond_to?(:quit)
67
72
  end
68
73
  rescue StandardError => e
69
- failures << Failure.new(index: nil, url: nil, error: e)
74
+ mutex.synchronize { failures << Failure.new(index: nil, url: nil, error: e) }
70
75
  end
71
76
  end
72
77
 
73
78
  threads.each(&:join)
74
- raise_for_failures(drain_queue(failures), results)
79
+ raise_for_failures(failures, results)
75
80
 
76
81
  results
77
82
  end
78
83
 
79
84
  private
80
85
 
81
- def drain_queue(queue)
82
- items = []
83
- loop do
84
- items << queue.pop(true)
85
- rescue ThreadError
86
- break
87
- end
88
- items
89
- end
90
-
91
86
  def raise_for_failures(failures, results)
92
87
  return if failures.empty?
93
88
 
@@ -1,10 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "cgi"
4
- require "net/http"
5
4
  require "nokogiri"
6
5
  require "uri"
7
6
 
7
+ require_relative "regulatory/http_client"
8
+
8
9
  module FetchUtil
9
10
  class RawDocsFallback
10
11
  DEFAULT_HEADERS = {
@@ -47,8 +48,9 @@ module FetchUtil
47
48
  ].freeze
48
49
  PRUNED_TEXT_PATTERN = /\A(?:on this page|table of contents|edit this page|copy page|copy item path|search|settings|help|expand description)\z/i
49
50
 
50
- def initialize(timeout: 20)
51
+ def initialize(timeout: 20, http_client: nil)
51
52
  @timeout = timeout.to_i
53
+ @http_client = http_client || FetchUtil::HttpRedirectClient.new(timeout: @timeout, headers: DEFAULT_HEADERS)
52
54
  end
53
55
 
54
56
  def fetch(url)
@@ -272,26 +274,11 @@ module FetchUtil
272
274
  %(concat(#{parts.join(%q(, "'", ))}))
273
275
  end
274
276
 
275
- def fetch_html(url, limit: 5)
276
- raise URI::InvalidURIError, "too many redirects" if limit <= 0
277
-
278
- uri = URI.parse(url)
279
- request = Net::HTTP::Get.new(uri)
280
- DEFAULT_HEADERS.each { |key, value| request[key] = value }
281
-
282
- response = Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https", open_timeout: @timeout, read_timeout: @timeout) do |http|
283
- http.request(request)
284
- end
277
+ def fetch_html(url)
278
+ response = @http_client.get(url)
279
+ raise Error, "HTTP #{response.status}" unless response.status.between?(200, 299)
285
280
 
286
- case response
287
- when Net::HTTPSuccess
288
- [uri.to_s, response.body]
289
- when Net::HTTPRedirection
290
- location = URI.join(uri, response["location"]).to_s
291
- fetch_html(location, limit: limit - 1)
292
- else
293
- raise Error, "HTTP #{response.code}"
294
- end
281
+ [response.url, response.body]
295
282
  end
296
283
  end
297
284
  end
@@ -1,70 +1,138 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "net/http"
4
+ require "uri"
4
5
 
5
6
  module FetchUtil
6
- class Regulatory
7
- class HttpClient
8
- REDIRECT_LIMIT = 5
7
+ class HttpRedirectClient
8
+ REDIRECT_LIMIT = 5
9
+ TRANSIENT_ERRORS = [EOFError, IOError, SocketError, SystemCallError, Timeout::Error].freeze
10
+ Response = Struct.new(:url, :status, :headers, :body, :redirects, keyword_init: true)
9
11
 
10
- def initialize(timeout:, user_agent:)
11
- @timeout = timeout.to_f
12
- @user_agent = user_agent.to_s.strip
13
- end
12
+ def initialize(timeout:, headers: {})
13
+ @timeout = timeout.to_f
14
+ @headers = headers.reject { |_key, value| value.to_s.empty? }
15
+ end
14
16
 
15
- def get(url, limit: REDIRECT_LIMIT)
16
- uri = parse_http_uri(url)
17
- fetch(uri, limit, [])
18
- end
17
+ def get(url, limit: REDIRECT_LIMIT)
18
+ @connections = {}
19
+ fetch(parse_http_uri(url), limit, [])
20
+ ensure
21
+ close_connections
22
+ end
19
23
 
20
- private
24
+ private
21
25
 
22
- attr_reader :timeout, :user_agent
26
+ attr_reader :timeout, :headers
23
27
 
24
- def fetch(uri, limit, redirects)
25
- response = request(uri)
26
- return build_response(uri, response, redirects: redirects) unless response.is_a?(Net::HTTPRedirection)
28
+ def fetch(uri, limit, redirects)
29
+ response = request(uri)
30
+ return build_response(uri, response, redirects: redirects) unless response.is_a?(Net::HTTPRedirection)
27
31
 
28
- raise FetchUtil::Error, "too many redirects for #{uri}" if limit <= 0
32
+ raise FetchUtil::Error, "too many redirects for #{uri}" if limit <= 0
29
33
 
30
- location = response["location"].to_s.strip
31
- return build_response(uri, response, redirects: redirects) if location.empty?
34
+ location = response["location"].to_s.strip
35
+ return build_response(uri, response, redirects: redirects) if location.empty?
32
36
 
33
- redirect_response = build_response(uri, response)
34
- fetch(uri.merge(location), limit - 1, redirects + [redirect_response])
35
- end
37
+ redirect_response = build_response(uri, response)
38
+ fetch(uri.merge(location), limit - 1, redirects + [redirect_response])
39
+ end
36
40
 
37
- def request(uri)
38
- http = Net::HTTP.new(uri.host, uri.port)
39
- http.use_ssl = uri.scheme == "https"
40
- http.open_timeout = timeout
41
- http.read_timeout = timeout
41
+ def request(uri)
42
+ attempts = 0
43
+ begin
44
+ http = connection_for(uri)
42
45
  request = Net::HTTP::Get.new(uri.request_uri.empty? ? "/" : uri.request_uri)
43
- request["Accept"] = "text/html,application/json,text/plain,*/*"
44
- request["User-Agent"] = user_agent unless user_agent.empty?
46
+ headers.each { |key, value| request[key] = value }
45
47
  http.request(request)
48
+ rescue *TRANSIENT_ERRORS
49
+ close_connection(uri)
50
+ attempts += 1
51
+ retry if attempts <= 1
52
+ raise
46
53
  end
54
+ end
47
55
 
48
- def build_response(uri, response, redirects: [])
49
- FetchUtil::Regulatory::Response.new(
50
- url: uri.to_s,
51
- status: response.code.to_i,
52
- headers: response.to_hash.transform_keys(&:downcase),
53
- body: response.body.to_s,
54
- redirects: redirects
55
- )
56
+ def connection_for(uri)
57
+ key = [uri.scheme, uri.host, uri.port]
58
+ @connections[key] ||= Net::HTTP.start(
59
+ uri.host,
60
+ uri.port,
61
+ use_ssl: uri.scheme == "https",
62
+ open_timeout: timeout,
63
+ read_timeout: timeout
64
+ )
65
+ end
66
+
67
+ def close_connection(uri)
68
+ key = [uri.scheme, uri.host, uri.port]
69
+ @connections.delete(key)&.finish
70
+ rescue IOError
71
+ nil
72
+ end
73
+
74
+ def close_connections
75
+ @connections&.each_value do |http|
76
+ http.finish if http.started?
77
+ rescue IOError
78
+ nil
56
79
  end
80
+ @connections = nil
81
+ end
82
+
83
+ def build_response(uri, response, redirects: [])
84
+ Response.new(
85
+ url: uri.to_s,
86
+ status: response.code.to_i,
87
+ headers: response.to_hash.transform_keys(&:downcase),
88
+ body: response.body.to_s,
89
+ redirects: redirects
90
+ )
91
+ end
92
+
93
+ def parse_http_uri(url)
94
+ uri = URI.parse(url.to_s)
95
+ raise URI::InvalidURIError, "unsupported url: #{url}" unless uri.is_a?(URI::HTTP) && uri.host
96
+
97
+ uri
98
+ end
99
+ end
57
100
 
58
- def parse_http_uri(url)
59
- uri = URI.parse(url.to_s)
60
- unless uri.is_a?(URI::HTTP) && uri.host
61
- raise ArgumentError, "unsupported url: #{url}"
62
- end
101
+ class Regulatory
102
+ class HttpClient
103
+ DEFAULT_ACCEPT = "text/html,application/json,text/plain,*/*"
104
+
105
+ def initialize(timeout:, user_agent:, redirect_client: nil)
106
+ @user_agent = user_agent.to_s.strip
107
+ @redirect_client = redirect_client || FetchUtil::HttpRedirectClient.new(timeout: timeout, headers: request_headers)
108
+ end
63
109
 
64
- uri
110
+ def get(url, limit: FetchUtil::HttpRedirectClient::REDIRECT_LIMIT)
111
+ build_response(redirect_client.get(url, limit: limit))
65
112
  rescue URI::InvalidURIError
66
113
  raise ArgumentError, "unsupported url: #{url}"
67
114
  end
115
+
116
+ private
117
+
118
+ attr_reader :redirect_client, :user_agent
119
+
120
+ def build_response(response)
121
+ FetchUtil::Regulatory::Response.new(
122
+ url: response.url,
123
+ status: response.status,
124
+ headers: response.headers,
125
+ body: response.body,
126
+ redirects: response.redirects.map { |redirect| build_response(redirect) }
127
+ )
128
+ end
129
+
130
+ def request_headers
131
+ {
132
+ "Accept" => DEFAULT_ACCEPT,
133
+ "User-Agent" => user_agent
134
+ }
135
+ end
68
136
  end
69
137
  end
70
138
  end
@@ -3,10 +3,13 @@
3
3
  module FetchUtil
4
4
  class Result
5
5
  attr_reader :url, :final_url, :title, :byline, :excerpt, :site_name,
6
- :published_time, :canonical_url, :language, :html, :markdown,
6
+ :published_time, :canonical_url, :language, :name, :company, :location,
7
+ :description, :ingredients, :instructions, :bedrooms, :bathrooms,
8
+ :area_sqft, :html, :markdown,
7
9
  :metadata, :reader_mode, :content_type, :suspect, :warnings,
8
10
  :content_completeness_ratio, :content_format, :paywall_state,
9
- :error_message
11
+ :price, :rating, :address, :social_kind, :platform, :handle,
12
+ :reply_count, :community, :score, :error_message
10
13
 
11
14
  class << self
12
15
  def from_payload(url:, final_url:, payload:, canonical_url:, content_type:, warnings:, suspect:)
@@ -22,7 +25,25 @@ module FetchUtil
22
25
  warnings: warnings,
23
26
  content_completeness_ratio: content_completeness_ratio,
24
27
  content_format: content_format,
25
- paywall_state: paywall_state
28
+ paywall_state: paywall_state,
29
+ name: payload["name"],
30
+ company: payload["company"],
31
+ location: payload["location"],
32
+ description: payload["description"],
33
+ ingredients: payload["ingredients"],
34
+ instructions: payload["instructions"],
35
+ bedrooms: payload["bedrooms"],
36
+ bathrooms: payload["bathrooms"],
37
+ area_sqft: payload["areaSqft"],
38
+ price: payload["price"],
39
+ rating: payload["rating"],
40
+ address: payload["address"],
41
+ social_kind: payload["socialKind"],
42
+ platform: payload["platform"],
43
+ handle: payload["handle"],
44
+ reply_count: payload["replyCount"],
45
+ community: payload["community"],
46
+ score: payload["score"]
26
47
  )
27
48
 
28
49
  new(
@@ -35,6 +56,15 @@ module FetchUtil
35
56
  published_time: payload["publishedTime"],
36
57
  canonical_url: canonical_url,
37
58
  language: payload["language"],
59
+ name: payload["name"],
60
+ company: payload["company"],
61
+ location: payload["location"],
62
+ description: payload["description"],
63
+ ingredients: payload["ingredients"],
64
+ instructions: payload["instructions"],
65
+ bedrooms: payload["bedrooms"],
66
+ bathrooms: payload["bathrooms"],
67
+ area_sqft: payload["areaSqft"],
38
68
  html: payload["html"],
39
69
  markdown: payload["markdown"],
40
70
  metadata: metadata,
@@ -44,7 +74,16 @@ module FetchUtil
44
74
  warnings: warnings,
45
75
  content_completeness_ratio: content_completeness_ratio,
46
76
  content_format: content_format,
47
- paywall_state: paywall_state
77
+ paywall_state: paywall_state,
78
+ price: payload["price"],
79
+ rating: payload["rating"],
80
+ address: payload["address"],
81
+ social_kind: payload["socialKind"],
82
+ platform: payload["platform"],
83
+ handle: payload["handle"],
84
+ reply_count: payload["replyCount"],
85
+ community: payload["community"],
86
+ score: payload["score"]
48
87
  )
49
88
  end
50
89
 
@@ -54,7 +93,13 @@ module FetchUtil
54
93
  content_type: "error",
55
94
  suspect: true,
56
95
  warnings: [warning],
57
- error_message: message
96
+ error_message: message,
97
+ social_kind: nil,
98
+ platform: nil,
99
+ handle: nil,
100
+ reply_count: nil,
101
+ community: nil,
102
+ score: nil
58
103
  }.freeze
59
104
 
60
105
  new(
@@ -67,6 +112,15 @@ module FetchUtil
67
112
  published_time: nil,
68
113
  canonical_url: nil,
69
114
  language: nil,
115
+ name: nil,
116
+ company: nil,
117
+ location: nil,
118
+ description: nil,
119
+ ingredients: nil,
120
+ instructions: nil,
121
+ bedrooms: nil,
122
+ bathrooms: nil,
123
+ area_sqft: nil,
70
124
  html: nil,
71
125
  markdown: "",
72
126
  metadata: metadata,
@@ -81,7 +135,10 @@ module FetchUtil
81
135
  private
82
136
 
83
137
  def payload_metadata(payload, canonical_url:, final_url:, content_type:, suspect:, warnings:,
84
- content_completeness_ratio:, content_format:, paywall_state:)
138
+ content_completeness_ratio:, content_format:, paywall_state:,
139
+ name:, company:, location:, description:, ingredients:, instructions:,
140
+ bedrooms:, bathrooms:, area_sqft:, price:, rating:, address:,
141
+ social_kind:, platform:, handle:, reply_count:, community:, score:)
85
142
  {
86
143
  title: payload["title"],
87
144
  byline: payload["byline"],
@@ -90,6 +147,15 @@ module FetchUtil
90
147
  published_time: payload["publishedTime"],
91
148
  canonical_url: canonical_url,
92
149
  language: payload["language"],
150
+ name: name,
151
+ company: company,
152
+ location: location,
153
+ description: description,
154
+ ingredients: ingredients,
155
+ instructions: instructions,
156
+ bedrooms: bedrooms,
157
+ bathrooms: bathrooms,
158
+ area_sqft: area_sqft,
93
159
  content_url: final_url,
94
160
  reader_mode: payload["readerMode"],
95
161
  content_type: content_type,
@@ -97,14 +163,27 @@ module FetchUtil
97
163
  warnings: warnings,
98
164
  content_completeness_ratio: content_completeness_ratio,
99
165
  content_format: content_format,
100
- paywall_state: paywall_state
166
+ paywall_state: paywall_state,
167
+ price: price,
168
+ rating: rating,
169
+ address: address,
170
+ social_kind: social_kind,
171
+ platform: platform,
172
+ handle: handle,
173
+ reply_count: reply_count,
174
+ community: community,
175
+ score: score
101
176
  }.freeze
102
177
  end
103
178
  end
104
179
 
105
180
  def initialize(url:, final_url:, title:, byline:, excerpt:, site_name:, published_time:,
106
- canonical_url:, language:, html:, markdown:, metadata:, reader_mode:, content_type:, suspect:, warnings:,
107
- content_completeness_ratio: 1.0, content_format: nil, paywall_state: nil, error_message: nil)
181
+ canonical_url:, language:, name:, company:, location:, description:, ingredients:,
182
+ instructions:, bedrooms:, bathrooms:, area_sqft:, html:, markdown:, metadata:,
183
+ reader_mode:, content_type:, suspect:, warnings:,
184
+ content_completeness_ratio: 1.0, content_format: nil, paywall_state: nil,
185
+ price: nil, rating: nil, address: nil, social_kind: nil, platform: nil,
186
+ handle: nil, reply_count: nil, community: nil, score: nil, error_message: nil)
108
187
  @url = url
109
188
  @final_url = final_url
110
189
  @title = title
@@ -114,6 +193,15 @@ module FetchUtil
114
193
  @published_time = published_time
115
194
  @canonical_url = canonical_url
116
195
  @language = language
196
+ @name = name
197
+ @company = company
198
+ @location = location
199
+ @description = description
200
+ @ingredients = ingredients
201
+ @instructions = instructions
202
+ @bedrooms = bedrooms
203
+ @bathrooms = bathrooms
204
+ @area_sqft = area_sqft
117
205
  @html = html
118
206
  @markdown = markdown
119
207
  @metadata = metadata.freeze
@@ -124,6 +212,15 @@ module FetchUtil
124
212
  @content_completeness_ratio = content_completeness_ratio
125
213
  @content_format = content_format&.freeze
126
214
  @paywall_state = paywall_state&.freeze
215
+ @price = price
216
+ @rating = rating
217
+ @address = address
218
+ @social_kind = social_kind
219
+ @platform = platform
220
+ @handle = handle
221
+ @reply_count = reply_count
222
+ @community = community
223
+ @score = score
127
224
  @error_message = error_message
128
225
  end
129
226
 
@@ -138,6 +235,15 @@ module FetchUtil
138
235
  published_time: published_time,
139
236
  canonical_url: canonical_url,
140
237
  language: language,
238
+ name: name,
239
+ company: company,
240
+ location: location,
241
+ description: description,
242
+ ingredients: ingredients,
243
+ instructions: instructions,
244
+ bedrooms: bedrooms,
245
+ bathrooms: bathrooms,
246
+ area_sqft: area_sqft,
141
247
  html: html,
142
248
  markdown: markdown,
143
249
  metadata: metadata,
@@ -148,6 +254,15 @@ module FetchUtil
148
254
  content_completeness_ratio: content_completeness_ratio,
149
255
  content_format: content_format,
150
256
  paywall_state: paywall_state,
257
+ price: price,
258
+ rating: rating,
259
+ address: address,
260
+ social_kind: social_kind,
261
+ platform: platform,
262
+ handle: handle,
263
+ reply_count: reply_count,
264
+ community: community,
265
+ score: score,
151
266
  error_message: error_message
152
267
  }
153
268
  end
@@ -5,8 +5,6 @@ require "uri"
5
5
 
6
6
  module FetchUtil
7
7
  class Searcher
8
- MAX_SNIPPET_LENGTH = 180
9
-
10
8
  SOURCES = {
11
9
  "duckduckgo" => "https://duckduckgo.com/?q=%<query>s&ia=web&kl=us-en",
12
10
  "google" => "https://www.google.com/search?hl=en&q=%<query>s",
@@ -21,10 +19,10 @@ module FetchUtil
21
19
  include ResultFiltering
22
20
  private_constant :ResultFiltering
23
21
 
24
- def initialize(fetcher: nil, request_log: RequestLog.new, sources: nil, limit: 10, concurrency: 2, verbose: false, **fetch_options)
22
+ def initialize(fetcher: nil, request_log: RequestLog.new, sources: nil, limit: nil, concurrency: 2, verbose: false, **fetch_options)
25
23
  @request_log = request_log
26
24
  @sources = Array(sources || DEFAULT_SOURCES).map(&:to_s)
27
- @limit = limit.to_i
25
+ @limit = limit
28
26
  @verbose = verbose
29
27
  @fetcher = fetcher || ParallelFetcher.new(concurrency: concurrency, request_log: request_log, **fetch_options)
30
28
  end
@@ -45,7 +43,7 @@ module FetchUtil
45
43
 
46
44
  {
47
45
  query: encoded_query,
48
- results: formatted_results(aggregate(urls.keys, fetched).first(limit))
46
+ results: formatted_results(apply_limit(aggregate(urls.keys, fetched)))
49
47
  }
50
48
  end
51
49
 
@@ -53,6 +51,10 @@ module FetchUtil
53
51
 
54
52
  attr_reader :limit
55
53
 
54
+ def apply_limit(results)
55
+ limit.nil? ? results : results.first(limit)
56
+ end
57
+
56
58
  def search_urls(query)
57
59
  urls = {}
58
60
 
@@ -131,7 +133,7 @@ module FetchUtil
131
133
  url: result[:url]
132
134
  }
133
135
  item[:snippet] = result[:snippet] if result[:snippet]
134
- if verbose?
136
+ if @verbose
135
137
  item[:sources] = result[:sources]
136
138
  item[:ranks] = result[:ranks]
137
139
  end
@@ -204,10 +206,6 @@ module FetchUtil
204
206
  item
205
207
  end
206
208
 
207
- def verbose?
208
- @verbose
209
- end
210
-
211
209
  def compact_text(value)
212
210
  FetchUtil.normalize_whitespace(value)
213
211
  end
@@ -296,13 +294,7 @@ module FetchUtil
296
294
  return nil if metadata_only_snippet?(text)
297
295
  return nil if jammed_navigation_text?(text)
298
296
 
299
- truncate(text, MAX_SNIPPET_LENGTH)
300
- end
301
-
302
- def truncate(text, max_length)
303
- return text if text.length <= max_length
304
-
305
- "#{text[0, max_length - 3].rstrip}..."
297
+ text
306
298
  end
307
299
 
308
300
  def domain_only?(text, host)
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module FetchUtil
4
- VERSION = "0.3.1"
4
+ VERSION = "0.4.0"
5
5
  end
data/lib/fetch_util.rb CHANGED
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "addressable/uri"
3
4
  require "uri"
4
5
 
5
6
  require_relative "fetch_util/version"
@@ -88,12 +89,21 @@ module FetchUtil
88
89
  text.gsub(/\u00A0/, " ").gsub(/\s+/, " ").strip
89
90
  end
90
91
 
92
+ def normalize_url(url)
93
+ return url if url.nil? || url.to_s.empty?
94
+
95
+ Addressable::URI.parse(url.to_s).normalize.to_s
96
+ rescue Addressable::URI::InvalidURIError, URI::InvalidURIError
97
+ url.to_s
98
+ end
99
+
91
100
  def strip_www_host(url)
92
- URI.parse(url.to_s).host.to_s.downcase.sub(/\Awww\./, "")
101
+ uri = url.is_a?(URI::Generic) ? url : URI.parse(url.to_s)
102
+ uri.host.to_s.downcase.sub(/\Awww\./, "")
93
103
  end
94
104
 
95
105
  def docs_like_url?(value)
96
- uri = value.is_a?(URI::Generic) ? value : URI.parse(value.to_s.strip)
106
+ uri = value.is_a?(URI::Generic) ? value : URI.parse(normalize_url(value.to_s.strip))
97
107
  return false unless uri.is_a?(URI::HTTP) && uri.host
98
108
 
99
109
  host = strip_www_host(uri)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fetch_util
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.1
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - hmdne
@@ -84,6 +84,7 @@ files:
84
84
  - lib/fetch_util/browser/site_stabilization/community_and_marketplace.rb
85
85
  - lib/fetch_util/browser/site_stabilization/gitlab_repo.rb
86
86
  - lib/fetch_util/browser/site_stabilization/social_platforms.rb
87
+ - lib/fetch_util/browser/site_stabilization/travel_and_lodging.rb
87
88
  - lib/fetch_util/browser/stabilization.rb
88
89
  - lib/fetch_util/browser/stabilization/page_flow.rb
89
90
  - lib/fetch_util/browser/stabilization/spa_hydration.rb
@@ -140,7 +141,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
140
141
  - !ruby/object:Gem::Version
141
142
  version: '0'
142
143
  requirements: []
143
- rubygems_version: 4.0.10
144
+ rubygems_version: 4.0.15
144
145
  specification_version: 4
145
146
  summary: AI for fetching in Ruby
146
147
  test_files: []