urlpipe 0.1.1 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ff32c6fa848dfe556a24917001d75e73954deaca028fd68cdc9996b739f1acb7
4
- data.tar.gz: 241d66afca02a3cd80c2a9f71b922eb9e9e5f95c5855bd57bf0e12a3a2a4d545
3
+ metadata.gz: 4bfaf07a0fb70edbee60c2467ec2f6123bc8c879c4eced1567f9ecbd51cf26d7
4
+ data.tar.gz: fc6ad0f280bc76a769a38383b4848ab98cf578360a7cbbb74a793dd94910571f
5
5
  SHA512:
6
- metadata.gz: 673c4a11f37ba6663153d34fe4d072e78bf936707399c8c3c7b93b29ca3e9406b921b9ddeb304735978a741c714344c5042f420faa2cd4b7d4a54c3f7ac0bb15
7
- data.tar.gz: b843a48d2ddaf91657a8dec8229c0bf2d54ce8f8fdce348bf849f81e16be978b546a9fca734b545c6e4f8f4c0becd50eb70701bb6f19773ba8aa5f5586e8c66b
6
+ metadata.gz: 6f8edccde9e41a55d017aa54c7d9da319212b6c074f37cc405d3efa37387345dc1ee956d690bf9d12cf840dbd5721f962cf233e491981b31f404836e8a58e0f5
7
+ data.tar.gz: fd35420d0c9087f63e65431b3c06092619201b538569a7d608b9c89559d6c7ba038a9712f16d6b5beae6563fa3d37002405154c04d66ea07c9686ca44c7930b9
data/CHANGELOG.md CHANGED
@@ -1,5 +1,15 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.0
4
+
5
+ - `pdf` returns the whole rendered page as a PDF (`POST /pdf`): a `Urlpipe::Pdf` with the decoded bytes, `mime_type`, `result_url` and `save`. `pdf_options:` choose the paper, orientation, margins, scale, backgrounds, the print or screen stylesheet and `single_page`.
6
+ - `extract` returns data from a page in a schema you define (`POST /extract`): pass `fields` and get a Hash with exactly those keys, in order, `nil` where the page gives no value. `instructions:` adds guidance in plain words.
7
+ - `scrape` takes `pdf_options:`, `fields:` and `instructions:` for its `pdf` and `extract` operations.
8
+ - `user_agent:` on `Urlpipe::Client.new` names the program making the requests; the User-Agent then reads `<product> urlpipe-ruby/<version>`.
9
+ - `technologies` detects what a page is built with (`POST /technologies`): its CMS, frameworks, analytics, CDN and web server, each with its version, a confidence and the signals it was detected by.
10
+ - `Meta#final_url` is the address the page ended on, after its redirects (the `X-Final-Url` header). A `/meta` result, a `/scrape` result and a webhook payload carry it as `"final_url"` too.
11
+ - `meta` returns the fixed shape of `/meta`: every key always present — `canonical_url`, `title`, `description`, `site_name`, `type`, `language`, `locale`, `authors`, `published_at`, `modified_at`, `image`, `video`, `favicon_url`, `icons`, `logo_url`, `feeds`, `keywords`, `theme_color`, `generator`, `manifest_url`, `robots`, `open_graph`, `twitter`, `alternates`, `oembed_url`, `structured_data_types`. `main_image_url`, `author_name`, `publication_date`, `feed_url` and `additional_author_information` are gone: read `image["url"]`, `authors`, `published_at` and `feeds`.
12
+
3
13
  ## 0.1.1
4
14
 
5
15
  - The gem's author is listed as URLpipe.
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # urlpipe
2
2
 
3
- Turn any URL into clean Markdown, rendered HTML, a full-page screenshot, metadata, a summary, keywords, console errors or a Lighthouse audit, from Ruby. Pages are rendered in real Chrome, so JavaScript-heavy sites come back complete.
3
+ Turn any URL into clean Markdown, rendered HTML, a full-page screenshot, a PDF, metadata, a summary, keywords, data in a schema you define, console errors, a Lighthouse audit or the technologies it is built with, from Ruby. Pages are rendered in real Chrome, so JavaScript-heavy sites come back complete.
4
4
 
5
5
  This is the official Ruby client for [URLpipe](https://urlpipe.dev). It has no runtime dependencies and runs on Ruby 3.1 and later.
6
6
 
@@ -37,7 +37,8 @@ Urlpipe::Client.new(
37
37
  timeout: 90, # seconds per HTTP request; the API holds a sync request up to 60 s
38
38
  max_retries: 2, # see "Retries and idempotency"
39
39
  wait_timeout: 300, # how long a long analysis is polled before Urlpipe::WaitTimeoutError
40
- poll_interval: 2 # seconds between polls of GET /result/:token
40
+ poll_interval: 2, # seconds between polls of GET /result/:token
41
+ user_agent: nil # your program's product token, e.g. "my-app/1.2"; it goes in front of the client's own
41
42
  )
42
43
  ```
43
44
 
@@ -54,7 +55,7 @@ Every method takes the URL first. These options work on all of them and are sent
54
55
  | `labels:` | Your own ids for the request, e.g. `{ client: "acme" }`. Returned with the result and the webhook. |
55
56
  | `residential:` | `true` fetches the page from a home broadband address. |
56
57
  | `report_to:` | A webhook URL the result is POSTed to (async calls). |
57
- | `page_options:` | `wait_for_selector`, `delay`, `block_ads`, `block_cookie_banners`, `remove_selectors`. Not on `lighthouse`. |
58
+ | `page_options:` | `wait_for_selector`, `delay`, `block_ads`, `block_cookie_banners`, `block_popups`, `remove_selectors`. Not on `lighthouse`. |
58
59
  | `idempotency_key:` | Sent as the `Idempotency-Key` header. |
59
60
  | `extra:` | A Hash merged into the request body, for API options this version doesn't name yet. |
60
61
 
@@ -82,12 +83,25 @@ shot.mime_type # => "image/webp"
82
83
  shot.result_url # => "https://..." - put it straight in an <img> tag
83
84
  ```
84
85
 
86
+ ### pdf
87
+
88
+ `data` is a `Urlpipe::Pdf`: the whole rendered page as decoded PDF bytes, and `result_url`, a link to the PDF that needs no API key. `pdf_options:` choose `paper_size`, `landscape`, `margin`, `scale`, `print_background`, `media` and `single_page`.
89
+
90
+ ```ruby
91
+ pdf = client.pdf("https://example.com/invoice", pdf_options: { paper_size: "letter", landscape: true }).data
92
+ pdf.save("invoice.pdf")
93
+ pdf.result_url # => "https://..."
94
+ ```
95
+
85
96
  ### meta
86
97
 
87
- `data` is a Hash with `title`, `description`, `language`, `main_image_url`, `favicon_url`, `author_name`, `feed_url`, `publication_date` and `additional_author_information`. Any of them can be `nil`.
98
+ `data` is a Hash with the same keys for every page: `final_url` (where the page ended up, after its redirects; relative URLs are resolved against it), `title`, `description`, `site_name`, `type`, `language`, `locale`, `authors`, `published_at`, `modified_at`, `image`, `video`, `favicon_url`, `icons`, `logo_url`, `feeds`, `keywords`, `canonical_url`, `robots`, `alternates`, `oembed_url`, `structured_data_types`, and the raw `open_graph` and `twitter` tags. A value the page doesn't declare is `nil` or an empty Array. URLs are absolute, dates ISO 8601.
88
99
 
89
100
  ```ruby
90
- client.meta("https://example.com").data["title"] # => "Example Domain"
101
+ page = client.meta("https://example.com").data
102
+ page["title"] # => "Example Domain"
103
+ page.dig("image", "url") # => "https://example.com/cover.jpg", or nil
104
+ page["authors"].map { _1["name"] }
91
105
  ```
92
106
 
93
107
  ### keywords
@@ -98,6 +112,24 @@ client.meta("https://example.com").data["title"] # => "Example Domain"
98
112
  client.keywords("https://example.com").data # => ["example domain", "documentation", ...]
99
113
  ```
100
114
 
115
+ ### extract
116
+
117
+ Data in a schema you define. Describe the fields you want and `data` is a Hash with exactly those keys, in that order, `nil` where the page gives no value. A field's `type` is `string`, `number`, `integer`, `boolean`, `date`, `datetime`, `url`, `email`, `object` (with its own `fields`) or `array` (with `items`).
118
+
119
+ ```ruby
120
+ product = client.extract(
121
+ "https://example.com/products/trail-shoe",
122
+ [
123
+ { name: "name", type: "string" },
124
+ { name: "price", type: "number", description: "The current price, without the currency" },
125
+ { name: "in_stock", type: "boolean" },
126
+ { name: "sizes", type: "array", items: "string" }
127
+ ],
128
+ instructions: "Ignore the related products."
129
+ ).data
130
+ product["price"] # => 89.5
131
+ ```
132
+
101
133
  ### console
102
134
 
103
135
  `data` is an Array of `{"type" => "error" | "warning" | "exception", "text" => "..."}`.
@@ -106,6 +138,15 @@ client.keywords("https://example.com").data # => ["example domain", "documentati
106
138
  client.console("https://example.com").data.select { |entry| entry["type"] == "exception" }
107
139
  ```
108
140
 
141
+ ### technologies
142
+
143
+ `data` is a Hash: `"final_url"` and `"technologies"`, an Array of what the page is built with, most certain first.
144
+
145
+ ```ruby
146
+ client.technologies("https://example.com").data["technologies"].map { |tech| [tech["name"], tech["version"]] }
147
+ # => [["WordPress", "6.4.2"], ["PHP", nil], ...]
148
+ ```
149
+
109
150
  ### lighthouse
110
151
 
111
152
  `data` is the audit as a Hash. `device:` is `"mobile"` (the default) or `"desktop"`; `include_audits: true` adds the full audits object.
@@ -117,7 +158,7 @@ report.dig("categories", "performance", "score")
117
158
 
118
159
  ### scrape
119
160
 
120
- Several operations off one page visit, so you get them all much sooner. `data` is `{"url" => ..., "operations" => {"markdown" => {"success", "result", "error", "cached"}, ...}}`. Each operation succeeds or fails on its own. A screenshot inside a scrape stays Base64, as the API returns it.
161
+ Several operations off one page visit, so you get them all much sooner. `data` is `{"url" => ..., "operations" => {"markdown" => {"success", "result", "error", "cached"}, ...}}`. Each operation succeeds or fails on its own. A screenshot or a PDF inside a scrape stays Base64, as the API returns it. `scrape` also takes `screenshot_options:`, `pdf_options:`, and `fields:` and `instructions:` for its `extract` operation.
121
162
 
122
163
  ```ruby
123
164
  page = client.scrape("https://example.com", %w[markdown meta screenshot],
@@ -135,7 +176,7 @@ Every method returns a `Urlpipe::Response`:
135
176
  | `data` | The result when completed, `nil` otherwise. |
136
177
  | `token` | The request's result token. Fetching the result again with it is free for 30 days. |
137
178
  | `labels` | The labels the request was made with; `{}` when it had none. |
138
- | `meta` | `cache` (`"hit"`, `"miss"`, `"partial"`), `cache_age`, `processing_time_ms`, `quota` (`cost`, `limit`, `remaining`, `overage`, `resets_at`), `concurrency_limit`, `result_url`, `idempotent_replayed`. |
179
+ | `meta` | `cache` (`"hit"`, `"miss"`, `"partial"`), `cache_age`, `processing_time_ms`, `quota` (`cost`, `limit`, `remaining`, `overage`, `resets_at`), `concurrency_limit`, `result_url`, `final_url` (where the page ended up, after its redirects), `idempotent_replayed`. |
139
180
 
140
181
  A value the API didn't send is `nil`; `idempotent_replayed` is always `true` or `false`. On an unlimited plan `quota.limit`, `quota.remaining` and `concurrency_limit` are the String `"unlimited"`.
141
182
 
@@ -156,7 +197,7 @@ report = client.wait(accepted.token, operation: "lighthouse").data
156
197
 
157
198
  `wait(token, operation: nil, timeout: wait_timeout, interval: poll_interval)` polls until the result is ready. `result(token, operation: nil)` checks once and returns a `"processing"` Response while it is still running.
158
199
 
159
- The API doesn't say which operation produced a token, so pass `operation:` to get `data` typed the way that method returns it. Without it JSON comes back parsed and text stays a String, which means a screenshot stays Base64: `client.wait(token, operation: "screenshot")` decodes it into a `Urlpipe::Screenshot`.
200
+ The API doesn't say which operation produced a token, so pass `operation:` to get `data` typed the way that method returns it. Without it JSON comes back parsed and text stays a String, which means a screenshot or a PDF stays Base64: `client.wait(token, operation: "screenshot")` decodes it into a `Urlpipe::Screenshot`, and `operation: "pdf"` into a `Urlpipe::Pdf`.
160
201
 
161
202
  ## Webhooks
162
203
 
@@ -176,7 +217,7 @@ class UrlpipeWebhooksController < ActionController::API
176
217
  end
177
218
  ```
178
219
 
179
- `verify(raw_body, headers, secret, tolerance: 300)` returns the payload Hash (`token`, `operation`, `labels`, `success`, `result`, `result_url`, `error`, `meta`), or raises `Urlpipe::WebhookVerificationError` saying why. It accepts any signature in the header that matches, so deliveries keep verifying through a secret rotation, and it refuses a timestamp more than `tolerance` seconds from now.
220
+ `verify(raw_body, headers, secret, tolerance: 300)` returns the payload Hash (`token`, `operation`, `labels`, `success`, `result`, `result_url`, `final_url`, `error`, `meta`), or raises `Urlpipe::WebhookVerificationError` saying why. It accepts any signature in the header that matches, so deliveries keep verifying through a secret rotation, and it refuses a timestamp more than `tolerance` seconds from now.
180
221
 
181
222
  Pass the **raw** request body, exactly the bytes received: `request.raw_post` in Rails, `request.body.read` in Rack. Parsed and re-serialized JSON has different bytes and will not verify. `headers` can be Rails' `request.headers`, a Rack env or a plain Hash.
182
223
 
@@ -18,7 +18,7 @@ module Urlpipe
18
18
  USER_AGENT = "urlpipe-ruby/#{VERSION}".freeze
19
19
 
20
20
  TEXT_OPERATIONS = %w[markdown html summarize].freeze
21
- JSON_OPERATIONS = %w[meta keywords console lighthouse scrape].freeze
21
+ JSON_OPERATIONS = %w[meta keywords extract console technologies lighthouse scrape].freeze
22
22
 
23
23
  RETRYABLE_STATUSES = [500, 502, 503].freeze
24
24
  MAX_RETRY_DELAY = 60
@@ -45,8 +45,10 @@ module Urlpipe
45
45
  # - +wait_timeout+: how long a sync call or +wait+ keeps polling a long
46
46
  # analysis before raising WaitTimeoutError
47
47
  # - +poll_interval+: seconds between polls of GET /result/:token
48
+ # - +user_agent+: your program's product token, such as "my-app/1.2". It
49
+ # goes in front of the client's own, so requests identify both
48
50
  def initialize(api_key: nil, base_url: DEFAULT_BASE_URL, timeout: 90, max_retries: 2,
49
- wait_timeout: 300, poll_interval: 2)
51
+ wait_timeout: 300, poll_interval: 2, user_agent: nil)
50
52
  api_key = ENV.fetch("URLPIPE_API_KEY", nil) if api_key.nil? || api_key.to_s.strip.empty?
51
53
  if api_key.nil? || api_key.strip.empty?
52
54
  raise ConfigurationError,
@@ -61,6 +63,7 @@ module Urlpipe
61
63
  @max_retries = Integer(max_retries)
62
64
  @wait_timeout = wait_timeout
63
65
  @poll_interval = poll_interval
66
+ @user_agent = [user_agent.to_s.strip, USER_AGENT].reject(&:empty?).join(" ")
64
67
  end
65
68
 
66
69
  def inspect
@@ -98,9 +101,22 @@ module Urlpipe
98
101
  idempotency_key, extra)
99
102
  end
100
103
 
101
- # Page metadata. +data+ is a Hash: title, description, language,
102
- # main_image_url, favicon_url, author_name, feed_url, publication_date,
103
- # additional_author_information.
104
+ # The whole rendered page as a PDF. +data+ is a Pdf (decoded bytes,
105
+ # +mime_type+, +result_url+). +pdf_options+ choose the paper, orientation,
106
+ # margins, scale and stylesheet.
107
+ def pdf(url, sync: true, max_age: nil, labels: nil, residential: nil, report_to: nil,
108
+ page_options: nil, pdf_options: nil, idempotency_key: nil, extra: {})
109
+ analyse("pdf", url,
110
+ { sync:, max_age:, labels:, residential:, report_to:, page_options:, pdf_options: },
111
+ idempotency_key, extra)
112
+ end
113
+
114
+ # Page metadata. +data+ is a Hash with the same keys for every page:
115
+ # final_url (where the page ended up, after its redirects), canonical_url,
116
+ # title, description, site_name, type, language, locale,
117
+ # authors, published_at, modified_at, image, video, favicon_url, icons,
118
+ # logo_url, feeds, keywords, theme_color, generator, manifest_url, robots,
119
+ # open_graph, twitter, alternates, oembed_url, structured_data_types.
104
120
  def meta(url, sync: true, max_age: nil, labels: nil, residential: nil, report_to: nil,
105
121
  page_options: nil, idempotency_key: nil, extra: {})
106
122
  analyse("meta", url, { sync:, max_age:, labels:, residential:, report_to:, page_options: },
@@ -114,6 +130,18 @@ module Urlpipe
114
130
  idempotency_key, extra)
115
131
  end
116
132
 
133
+ # Data from the page in the schema +fields+ describes: an Array of Hashes
134
+ # with :name, :type and optionally :description, :fields (an object's) and
135
+ # :items (an array's). +data+ is a Hash with exactly those names as keys,
136
+ # in order, nil where the page gives no value. +instructions+ adds
137
+ # guidance in plain words.
138
+ def extract(url, fields, sync: true, max_age: nil, labels: nil, residential: nil, report_to: nil,
139
+ page_options: nil, instructions: nil, idempotency_key: nil, extra: {})
140
+ analyse("extract", url,
141
+ { fields:, instructions:, sync:, max_age:, labels:, residential:, report_to:, page_options: },
142
+ idempotency_key, extra)
143
+ end
144
+
117
145
  # Console errors, warnings and uncaught exceptions. +data+ is an Array of
118
146
  # Hashes with "type" ("error", "warning" or "exception") and "text".
119
147
  def console(url, sync: true, max_age: nil, labels: nil, residential: nil, report_to: nil,
@@ -122,6 +150,15 @@ module Urlpipe
122
150
  idempotency_key, extra)
123
151
  end
124
152
 
153
+ # What the page is built with. +data+ is a Hash with "final_url" and
154
+ # "technologies", an Array of Hashes with "name", "version", "confidence",
155
+ # "categories", "detected_by" and the rest, most certain first.
156
+ def technologies(url, sync: true, max_age: nil, labels: nil, residential: nil, report_to: nil,
157
+ page_options: nil, idempotency_key: nil, extra: {})
158
+ analyse("technologies", url, { sync:, max_age:, labels:, residential:, report_to:, page_options: },
159
+ idempotency_key, extra)
160
+ end
161
+
125
162
  # A Lighthouse audit. +data+ is a Hash. +device+ is "mobile" (the API's
126
163
  # default) or "desktop".
127
164
  def lighthouse(url, sync: true, max_age: nil, labels: nil, residential: nil, report_to: nil,
@@ -132,14 +169,15 @@ module Urlpipe
132
169
 
133
170
  # Several operations off one page visit. +operations+ is an Array such as
134
171
  # ["markdown", "meta"]. +data+ is a Hash:
135
- # {"url" => ..., "operations" => {"markdown" => {"success", "result", "error", "cached"}}}.
136
- # A screenshot inside a scrape stays Base64, as the API returns it.
172
+ # {"url" => ..., "final_url" => ..., "operations" => {"markdown" => {"success", "result", "error", "cached"}}}.
173
+ # A screenshot or a PDF inside a scrape stays Base64, as the API returns
174
+ # it. +fields+ and +instructions+ are for its "extract" operation.
137
175
  def scrape(url, operations, sync: true, max_age: nil, labels: nil, residential: nil, report_to: nil,
138
176
  page_options: nil, device: nil, include_audits: nil, screenshot_options: nil,
139
- idempotency_key: nil, extra: {})
177
+ pdf_options: nil, fields: nil, instructions: nil, idempotency_key: nil, extra: {})
140
178
  analyse("scrape", url,
141
179
  { operations:, sync:, max_age:, labels:, residential:, report_to:, page_options:,
142
- device:, include_audits:, screenshot_options: },
180
+ device:, include_audits:, screenshot_options:, pdf_options:, fields:, instructions: },
143
181
  idempotency_key, extra)
144
182
  end
145
183
 
@@ -148,8 +186,9 @@ module Urlpipe
148
186
  #
149
187
  # +operation+ names the operation that produced the token, so +data+ is
150
188
  # typed as that operation returns it. Without it, JSON stays parsed JSON
151
- # and text stays a String (a screenshot stays Base64: pass
152
- # <tt>operation: "screenshot"</tt> to get a Screenshot).
189
+ # and text stays a String (a screenshot or a PDF stays Base64: pass
190
+ # <tt>operation: "screenshot"</tt> or <tt>"pdf"</tt> to get a Screenshot
191
+ # or a Pdf).
153
192
  def result(token, operation: nil)
154
193
  raw = execute(:get, "/result/#{URI.encode_www_form_component(token)}")
155
194
 
@@ -234,6 +273,8 @@ module Urlpipe
234
273
  raw.text
235
274
  elsif operation == "screenshot"
236
275
  Screenshot.from_base64(raw.text, result_url: raw.headers["x-result-url"])
276
+ elsif operation == "pdf"
277
+ Pdf.from_base64(raw.text, result_url: raw.headers["x-result-url"])
237
278
  elsif JSON_OPERATIONS.include?(operation) || (operation.nil? && raw.json?)
238
279
  parse_result_json(raw)
239
280
  else
@@ -316,7 +357,7 @@ module Urlpipe
316
357
 
317
358
  request = method == :post ? Net::HTTP::Post.new(uri) : Net::HTTP::Get.new(uri)
318
359
  request["Authorization"] = "Bearer #{@api_key}"
319
- request["User-Agent"] = USER_AGENT
360
+ request["User-Agent"] = @user_agent
320
361
  request["Accept"] = "*/*"
321
362
  request["Idempotency-Key"] = idempotency_key if idempotency_key
322
363
  if body
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Urlpipe
4
+ # A decoded PDF of the page: +data+ is the PDF as a binary String,
5
+ # +mime_type+ is "application/pdf", and +result_url+ is a link to the same
6
+ # PDF that needs no API key.
7
+ class Pdf
8
+ MIME_TYPE = "application/pdf"
9
+
10
+ attr_reader :data, :result_url
11
+
12
+ # Builds a Pdf from the API's Base64 body.
13
+ def self.from_base64(text, result_url: nil)
14
+ new(text.to_s.unpack1("m"), result_url: result_url)
15
+ end
16
+
17
+ def initialize(data, result_url: nil)
18
+ @data = data.b
19
+ @result_url = result_url
20
+ end
21
+
22
+ def mime_type = MIME_TYPE
23
+
24
+ # Writes the PDF to +path+ and returns the path.
25
+ def save(path)
26
+ File.binwrite(path, data)
27
+ path
28
+ end
29
+
30
+ def bytesize = data.bytesize
31
+
32
+ def inspect
33
+ "#<#{self.class.name} #{bytesize} bytes result_url=#{result_url.inspect}>"
34
+ end
35
+ end
36
+ end
@@ -16,11 +16,12 @@ module Urlpipe
16
16
  # - +quota+: a Quota
17
17
  # - +concurrency_limit+: Integer or "unlimited"
18
18
  # - +result_url+: screenshots only, a link to the image that needs no key
19
+ # - +final_url+: where the page ended up, after its redirects
19
20
  # - +idempotent_replayed+: +true+ when this answers an earlier request with
20
21
  # the same Idempotency-Key, +false+ otherwise
21
22
  Meta = Struct.new(
22
23
  :cache, :cache_age, :processing_time_ms, :quota, :concurrency_limit,
23
- :result_url, :idempotent_replayed,
24
+ :result_url, :final_url, :idempotent_replayed,
24
25
  keyword_init: true
25
26
  ) do
26
27
  # +headers+ is a Hash of lowercase header names to values.
@@ -38,6 +39,7 @@ module Urlpipe
38
39
  ),
39
40
  concurrency_limit: integer_or_given(headers["x-concurrency-limit"]),
40
41
  result_url: headers["x-result-url"],
42
+ final_url: headers["x-final-url"],
41
43
  idempotent_replayed: headers["idempotent-replayed"].to_s.strip.casecmp?("true")
42
44
  )
43
45
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Urlpipe
4
- VERSION = "0.1.1"
4
+ VERSION = "0.2.0"
5
5
  end
@@ -22,7 +22,8 @@ module Urlpipe
22
22
 
23
23
  # Checks the delivery's signature and freshness and returns the parsed
24
24
  # payload, a Hash with "token", "operation", "labels", "success",
25
- # "result", "result_url", "error" and "meta".
25
+ # "result", "result_url", "final_url" (where the page ended up, after its
26
+ # redirects), "error" and "meta".
26
27
  #
27
28
  # - +raw_body+: the request body as received (a String, or an IO)
28
29
  # - +headers+: the request headers. Accepts a plain Hash, Rails'
data/lib/urlpipe.rb CHANGED
@@ -4,12 +4,14 @@ require_relative "urlpipe/version"
4
4
  require_relative "urlpipe/errors"
5
5
  require_relative "urlpipe/response"
6
6
  require_relative "urlpipe/screenshot"
7
+ require_relative "urlpipe/pdf"
7
8
  require_relative "urlpipe/client"
8
9
  require_relative "urlpipe/webhook"
9
10
 
10
11
  # Ruby client for the URLpipe API (https://urlpipe.dev): turn a URL into
11
- # Markdown, rendered HTML, a screenshot, metadata, a summary, keywords,
12
- # console errors or a Lighthouse audit.
12
+ # Markdown, rendered HTML, a screenshot, a PDF, metadata, a summary, keywords,
13
+ # data in a schema you define, console errors, a Lighthouse audit or the
14
+ # technologies a site is built with.
13
15
  #
14
16
  # client = Urlpipe::Client.new # reads URLPIPE_API_KEY
15
17
  # client.markdown("https://example.com").data
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: urlpipe
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - URLpipe
@@ -10,9 +10,9 @@ cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies: []
12
12
  description: Official Ruby client for URLpipe. Get a page's Markdown, rendered HTML,
13
- a full-page screenshot, metadata, a summary, keywords, console errors or a Lighthouse
14
- audit, rendered in real Chrome. Handles long analyses, retries with idempotency
15
- keys, and webhook signature verification. No runtime dependencies.
13
+ a full-page screenshot, a PDF, metadata, a summary, keywords, structured data, console
14
+ errors or a Lighthouse audit, rendered in real Chrome. Handles long analyses, retries
15
+ with idempotency keys, and webhook signature verification. No runtime dependencies.
16
16
  email:
17
17
  - contact@urlpipe.dev
18
18
  executables: []
@@ -25,6 +25,7 @@ files:
25
25
  - lib/urlpipe.rb
26
26
  - lib/urlpipe/client.rb
27
27
  - lib/urlpipe/errors.rb
28
+ - lib/urlpipe/pdf.rb
28
29
  - lib/urlpipe/response.rb
29
30
  - lib/urlpipe/screenshot.rb
30
31
  - lib/urlpipe/version.rb