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 +4 -4
- data/CHANGELOG.md +10 -0
- data/README.md +50 -9
- data/lib/urlpipe/client.rb +53 -12
- data/lib/urlpipe/pdf.rb +36 -0
- data/lib/urlpipe/response.rb +3 -1
- data/lib/urlpipe/version.rb +1 -1
- data/lib/urlpipe/webhook.rb +2 -1
- data/lib/urlpipe.rb +4 -2
- metadata +5 -4
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 4bfaf07a0fb70edbee60c2467ec2f6123bc8c879c4eced1567f9ecbd51cf26d7
|
|
4
|
+
data.tar.gz: fc6ad0f280bc76a769a38383b4848ab98cf578360a7cbbb74a793dd94910571f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
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
|
|
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`, `
|
|
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
|
|
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
|
|
data/lib/urlpipe/client.rb
CHANGED
|
@@ -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
|
-
#
|
|
102
|
-
#
|
|
103
|
-
#
|
|
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
|
|
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"] =
|
|
360
|
+
request["User-Agent"] = @user_agent
|
|
320
361
|
request["Accept"] = "*/*"
|
|
321
362
|
request["Idempotency-Key"] = idempotency_key if idempotency_key
|
|
322
363
|
if body
|
data/lib/urlpipe/pdf.rb
ADDED
|
@@ -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
|
data/lib/urlpipe/response.rb
CHANGED
|
@@ -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
|
data/lib/urlpipe/version.rb
CHANGED
data/lib/urlpipe/webhook.rb
CHANGED
|
@@ -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", "
|
|
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
|
|
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.
|
|
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,
|
|
14
|
-
audit, rendered in real Chrome. Handles long analyses, retries
|
|
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
|