ghostcrawl 2.2.3 → 2.3.1
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/LICENSE +21 -46
- data/README.md +96 -4
- data/_generated/v1/binary/binary.rb +9 -0
- data/_generated/v1/v1_request_builder.rb +1 -6
- data/lib/ghostcrawl/client.rb +396 -28
- data/lib/ghostcrawl/version.rb +1 -1
- metadata +3 -6
- data/_generated/v1/binary/binary_request_builder.rb +0 -31
- data/_generated/v1/binary/download/download.rb +0 -0
- data/_generated/v1/binary/download/download_request_builder.rb +0 -77
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9b83f5c591c7a3ffd859db07f96c9725f84db05826b65e9227eadce1ff3eded3
|
|
4
|
+
data.tar.gz: 0aa598b5c6d776b5477c0709aa26dd3789d5f79084421b1fae802e90892c4dc0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3458805483c931bbd9adca163e74d0610c7d02abdb6ca260459fbdbe162c573f29752846d92bf45189a8fcea7957f770588364c9c483082729386c8b2d7c281a
|
|
7
|
+
data.tar.gz: 8d56532b83ea407f5a8e9190832fe1fd8ae3327cd427cb3f26be0fd727c20567a5bc1331998caa403d622edd7456956b5a373ae1961d1281f336b741ea607b32
|
data/LICENSE
CHANGED
|
@@ -1,46 +1,21 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 GhostCrawl, LLC
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
party, including as a hosted or managed service; (d) reverse engineer,
|
|
23
|
-
decompile, disassemble, or otherwise attempt to derive the source code,
|
|
24
|
-
structure, or internal design of any compiled component of the Software;
|
|
25
|
-
(e) circumvent, disable, or tamper with any license, authentication,
|
|
26
|
-
usage-metering, watermarking, or security mechanism; or (f) remove, alter, or
|
|
27
|
-
obscure any proprietary, copyright, or attribution notice.
|
|
28
|
-
|
|
29
|
-
3. OWNERSHIP. GhostCrawl and its licensors retain all right, title, and interest
|
|
30
|
-
in and to the Software and all intellectual property rights therein. No rights
|
|
31
|
-
are granted to you except as expressly set out in this License. All rights
|
|
32
|
-
reserved.
|
|
33
|
-
|
|
34
|
-
4. TERMINATION. This License terminates automatically, without notice, if you
|
|
35
|
-
breach any of its terms or cease to maintain a valid GhostCrawl account where
|
|
36
|
-
one is required. Upon termination you must cease all use of the Software and
|
|
37
|
-
destroy all copies in your possession or control.
|
|
38
|
-
|
|
39
|
-
5. NO WARRANTY; LIMITATION OF LIABILITY. THE SOFTWARE IS PROVIDED "AS IS" WITHOUT
|
|
40
|
-
WARRANTY OF ANY KIND, EXPRESS OR IMPLIED. TO THE MAXIMUM EXTENT PERMITTED BY
|
|
41
|
-
LAW, GHOSTCRAWL AND ITS LICENSORS WILL NOT BE LIABLE FOR ANY CLAIM, DAMAGES, OR
|
|
42
|
-
OTHER LIABILITY ARISING FROM OR IN CONNECTION WITH THE SOFTWARE OR ITS USE. The
|
|
43
|
-
GhostCrawl Terms of Service and Acceptable Use Policy
|
|
44
|
-
(https://docs.ghostcrawl.io/legal/terms) are incorporated by reference.
|
|
45
|
-
|
|
46
|
-
For licensing inquiries, contact legal@ghostcrawl.io.
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 GhostCrawl, LLC
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
CHANGED
|
@@ -29,9 +29,11 @@ client = Ghostcrawl::Client.new(token: "gck_live_YOUR_KEY")
|
|
|
29
29
|
result = client.scrape(url: "https://example.com", format: "markdown")
|
|
30
30
|
puts result["content"]
|
|
31
31
|
|
|
32
|
-
# Start a crawl run
|
|
33
|
-
|
|
34
|
-
|
|
32
|
+
# Start a crawl run and wait for it to finish — the server blocks until the run
|
|
33
|
+
# is terminal (no client-side poll-with-sleep loop).
|
|
34
|
+
run = client.crawl_runs.start(url: "https://example.com", max_depth: 2, max_pages: 50,
|
|
35
|
+
wait: true, timeout: 300)
|
|
36
|
+
puts "#{run["run_id"]} → #{run["status"]}"
|
|
35
37
|
|
|
36
38
|
# Web search
|
|
37
39
|
results = client.search(query: "latest AI research", engine: "google", limit: 10)
|
|
@@ -72,6 +74,92 @@ data = client.extract(
|
|
|
72
74
|
puts "#{data["name"]} — $#{data["price"]}"
|
|
73
75
|
```
|
|
74
76
|
|
|
77
|
+
## Browser utilities — content, screenshot, PDF
|
|
78
|
+
|
|
79
|
+
```ruby
|
|
80
|
+
require "ghostcrawl"
|
|
81
|
+
|
|
82
|
+
client = Ghostcrawl::Client.new(token: "gck_live_YOUR_KEY")
|
|
83
|
+
|
|
84
|
+
# Rendered content as a JSON envelope: {url, status, format, status_code, content, bytes}
|
|
85
|
+
page = client.content(url: "https://example.com")
|
|
86
|
+
puts "#{page["status_code"]} — #{page["bytes"]} bytes"
|
|
87
|
+
|
|
88
|
+
# Screenshot — returns raw PNG bytes
|
|
89
|
+
png = client.screenshot(url: "https://example.com", full_page: true)
|
|
90
|
+
File.binwrite("page.png", png)
|
|
91
|
+
|
|
92
|
+
# PDF — returns raw application/pdf bytes (Chrome-only; a Firefox/WebKit identity
|
|
93
|
+
# is rejected with a 400 pdf_engine_unsupported)
|
|
94
|
+
pdf = client.pdf(url: "https://example.com", paper_format: "a4")
|
|
95
|
+
File.binwrite("page.pdf", pdf)
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Agent (BYO model, account-gated)
|
|
99
|
+
|
|
100
|
+
The agent lane runs a natural-language browser task. It is **bring-your-own-model** (supply your
|
|
101
|
+
own `provider_config` in the request body) and **account-gated**: the API returns `404 not_found`
|
|
102
|
+
unless the capability is enabled for your account. `agent` does **not** raise on that 404 — it
|
|
103
|
+
returns the `problem+json` body as a Hash, so branch on `result.key?("detail")`.
|
|
104
|
+
|
|
105
|
+
```ruby
|
|
106
|
+
require "ghostcrawl"
|
|
107
|
+
|
|
108
|
+
client = Ghostcrawl::Client.new(token: "gck_live_YOUR_KEY")
|
|
109
|
+
|
|
110
|
+
# provider_config is BYO — reference your provider key by env-var name, never a literal.
|
|
111
|
+
result = client.agent(
|
|
112
|
+
task: {
|
|
113
|
+
"instruction" => "click the 'Books to Scrape' link",
|
|
114
|
+
"start_url" => "https://books.toscrape.com"
|
|
115
|
+
},
|
|
116
|
+
provider_config: {
|
|
117
|
+
"provider" => "openai",
|
|
118
|
+
"api_key" => "OPENAI_API_KEY",
|
|
119
|
+
"model" => "gpt-4o"
|
|
120
|
+
}
|
|
121
|
+
)
|
|
122
|
+
if result.key?("detail")
|
|
123
|
+
puts "agent lane not enabled for this account (BYO/gated)"
|
|
124
|
+
else
|
|
125
|
+
puts result
|
|
126
|
+
end
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## Crawl runs — wait for completion
|
|
130
|
+
|
|
131
|
+
Crawl runs are asynchronous. Instead of hand-writing a poll-with-sleep loop, use
|
|
132
|
+
the event-driven wait API: the **server** blocks until the run reaches a terminal
|
|
133
|
+
state (`completed` / `failed` / `cancelled`) and returns the finished run.
|
|
134
|
+
|
|
135
|
+
```ruby
|
|
136
|
+
# Start and wait in one call — sends wait_until: "completed"
|
|
137
|
+
run = client.crawl_runs.start(
|
|
138
|
+
url: "https://example.com",
|
|
139
|
+
max_depth: 2,
|
|
140
|
+
max_pages: 50,
|
|
141
|
+
wait: true, # block until terminal (no client sleep loop)
|
|
142
|
+
timeout: 300 # give up after 300s, returning the still-running run
|
|
143
|
+
)
|
|
144
|
+
puts run["status"] # "completed" | "failed" | "cancelled"
|
|
145
|
+
|
|
146
|
+
# Or wait on a run you already started (e.g. from a webhook or another process):
|
|
147
|
+
run = client.crawl_runs.wait_for_completion(run_id, timeout: 300)
|
|
148
|
+
|
|
149
|
+
# Deep crawl supports the same wait: keyword:
|
|
150
|
+
run = client.crawl(url: "https://example.com", wait: true, timeout: 300)
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Under the hood `wait_for_completion` chains server-blocking
|
|
154
|
+
`GET /v1/crawl-runs/{run_id}?wait=true&timeout_s=N` requests until the run is
|
|
155
|
+
terminal or your `timeout` deadline — there is no `sleep` between checks. On
|
|
156
|
+
timeout the current non-terminal run is returned, so you can simply call
|
|
157
|
+
`wait_for_completion` again to keep waiting.
|
|
158
|
+
|
|
159
|
+
**Prefer webhooks for fire-and-forget crawls.** Register an endpoint with
|
|
160
|
+
`client.webhooks.create(url:, event_types: ["crawl.completed"])` and let GhostCrawl
|
|
161
|
+
POST the result to you instead of holding a connection open.
|
|
162
|
+
|
|
75
163
|
## Error handling
|
|
76
164
|
|
|
77
165
|
```ruby
|
|
@@ -112,8 +200,12 @@ client = Ghostcrawl::Client.new(
|
|
|
112
200
|
| Data extraction | `client.extract(url:, schema:)` | Structured JSON from any page |
|
|
113
201
|
| Deep crawl | `client.crawl(url:)` | Crawl a site depth-first |
|
|
114
202
|
| URL map | `client.map(url:)` | Discover all reachable URLs |
|
|
203
|
+
| Content | `client.content(url:)` | Rendered content JSON envelope |
|
|
204
|
+
| Screenshot | `client.screenshot(url:)` | Capture a URL to PNG bytes |
|
|
205
|
+
| PDF | `client.pdf(url:)` | Render a URL to PDF bytes (Chrome-only) |
|
|
206
|
+
| Agent (BYO) | `client.agent(task:)` | NL browser task — account-gated, BYO model |
|
|
115
207
|
| Account | `client.me` | Get account info and usage |
|
|
116
|
-
| Crawl runs | `client.crawl_runs` | start, list, get
|
|
208
|
+
| Crawl runs | `client.crawl_runs` | start (with `wait:`), wait\_for\_completion, list, get, cancel |
|
|
117
209
|
| Sessions | `client.sessions` | create, list, extend, release |
|
|
118
210
|
| Profiles | `client.profiles` | list, get\_profile, create, update, delete |
|
|
119
211
|
| Webhooks | `client.webhooks` | list, get\_webhook, create, delete, rotate\_secret |
|
|
@@ -2,7 +2,7 @@ require 'microsoft_kiota_abstractions'
|
|
|
2
2
|
require_relative '../ghostcrawl'
|
|
3
3
|
require_relative './audit/audit_request_builder'
|
|
4
4
|
require_relative './billing/billing_request_builder'
|
|
5
|
-
require_relative './binary/
|
|
5
|
+
require_relative './binary/binary'
|
|
6
6
|
require_relative './budgets/budgets_request_builder'
|
|
7
7
|
require_relative './cdp/cdp_request_builder'
|
|
8
8
|
require_relative './contact/contact_request_builder'
|
|
@@ -50,11 +50,6 @@ module Ghostcrawl
|
|
|
50
50
|
return Ghostcrawl::V1::Billing::BillingRequestBuilder.new(@path_parameters, @request_adapter)
|
|
51
51
|
end
|
|
52
52
|
##
|
|
53
|
-
# The binary property
|
|
54
|
-
def binary()
|
|
55
|
-
return Ghostcrawl::V1::Binary::BinaryRequestBuilder.new(@path_parameters, @request_adapter)
|
|
56
|
-
end
|
|
57
|
-
##
|
|
58
53
|
# The budgets property
|
|
59
54
|
def budgets()
|
|
60
55
|
return Ghostcrawl::V1::Budgets::BudgetsRequestBuilder.new(@path_parameters, @request_adapter)
|
data/lib/ghostcrawl/client.rb
CHANGED
|
@@ -381,6 +381,47 @@ module Ghostcrawl
|
|
|
381
381
|
Ghostcrawl.raise_translated(e)
|
|
382
382
|
end
|
|
383
383
|
|
|
384
|
+
# Executes a request whose success response is RAW BINARY (e.g. the
|
|
385
|
+
# +application/pdf+ body from +POST /v1/pdf+) and returns the bytes verbatim.
|
|
386
|
+
#
|
|
387
|
+
# Mirrors {void_request!}: it reuses the adapter's own request pipeline
|
|
388
|
+
# (base URL + bearer auth via +authenticate_request+, then +run_request+) but
|
|
389
|
+
# deliberately skips Kiota body deserialization — the response is not JSON, so
|
|
390
|
+
# there is no parse node to build. A 2xx returns the raw response body (an
|
|
391
|
+
# ASCII-8BIT String of bytes, ready to write to a file); a >=400 is translated
|
|
392
|
+
# into the documented typed {GhostcrawlError} hierarchy (the problem+json
|
|
393
|
+
# +detail+ rides along in the message).
|
|
394
|
+
#
|
|
395
|
+
# @param adapter [MicrosoftKiotaFaraday::FaradayRequestAdapter]
|
|
396
|
+
# @param request_info [MicrosoftKiotaAbstractions::RequestInformation]
|
|
397
|
+
# @return [String] the raw response bytes (ASCII-8BIT)
|
|
398
|
+
# @api private
|
|
399
|
+
def self.binary_request!(adapter, request_info)
|
|
400
|
+
request_info.path_parameters["baseurl"] = adapter.get_base_url
|
|
401
|
+
auth_fiber = adapter.authentication_provider.authenticate_request(request_info)
|
|
402
|
+
auth_fiber.resume if auth_fiber.respond_to?(:resume)
|
|
403
|
+
request = adapter.get_request_from_request_info(request_info)
|
|
404
|
+
response = adapter.client.run_request(
|
|
405
|
+
request.http_method, request.path, request.body, request.headers
|
|
406
|
+
)
|
|
407
|
+
|
|
408
|
+
status = response.status
|
|
409
|
+
if status >= 400
|
|
410
|
+
body = response.body.to_s
|
|
411
|
+
err = MicrosoftKiotaAbstractions::ApiError.new(
|
|
412
|
+
"The server returned an unexpected status code:#{status}" \
|
|
413
|
+
"#{body.empty? ? '' : " #{body}"}"
|
|
414
|
+
)
|
|
415
|
+
Ghostcrawl.raise_translated(err)
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
response.body.to_s.b
|
|
419
|
+
rescue Ghostcrawl::GhostcrawlError
|
|
420
|
+
raise
|
|
421
|
+
rescue StandardError => e
|
|
422
|
+
Ghostcrawl.raise_translated(e)
|
|
423
|
+
end
|
|
424
|
+
|
|
384
425
|
# Inspects a decoded HTTP-200 response hash for a RESULT-channel failure (the
|
|
385
426
|
# target page could not be scraped) and raises {Ghostcrawl::ScrapeError} when
|
|
386
427
|
# one is present. This is the reliable, highest-value error path: the body is
|
|
@@ -463,26 +504,101 @@ module Ghostcrawl
|
|
|
463
504
|
|
|
464
505
|
# Manage crawl runs — /v1/crawl-runs.
|
|
465
506
|
class CrawlRunsClient
|
|
466
|
-
|
|
507
|
+
# Terminal run states — a crawl run in any of these will never change again,
|
|
508
|
+
# so a wait can stop. Both spellings of "cancelled" are accepted.
|
|
509
|
+
TERMINAL_STATUSES = %w[completed failed cancelled canceled].freeze
|
|
510
|
+
|
|
511
|
+
# Per-request server-block window (seconds) for the long-poll wait. Each
|
|
512
|
+
# +GET ...?wait=true&timeout_s=WAIT_WINDOW_S+ makes the SERVER block for up
|
|
513
|
+
# to this long, so the client never sleeps between checks. Kept well under
|
|
514
|
+
# the client read timeout ({Client::DEFAULT_TIMEOUT}) so a single blocking
|
|
515
|
+
# request can never trip the transport timeout.
|
|
516
|
+
WAIT_WINDOW_S = 30
|
|
517
|
+
|
|
518
|
+
def initialize(v1, adapter = nil)
|
|
467
519
|
@v1 = v1
|
|
520
|
+
@adapter = adapter
|
|
468
521
|
end
|
|
469
522
|
|
|
470
523
|
# Start a new crawl run from a seed URL.
|
|
471
524
|
# Delegates to POST /v1/crawl-runs via the generated CrawlRunsRequestBuilder.
|
|
472
525
|
# The endpoint is a tagged union: a start request requires +action: "start"+
|
|
473
526
|
# and a +seed_urls+ array (not a bare +url+).
|
|
527
|
+
#
|
|
528
|
+
# Pass +wait: true+ to block until the run reaches a terminal state
|
|
529
|
+
# (completed | failed | cancelled) or +timeout+ seconds elapse. This sends
|
|
530
|
+
# +wait_until: "completed"+ so the SERVER blocks — no client-side poll loop,
|
|
531
|
+
# no +sleep+. When the run is still running at +timeout+ the current
|
|
532
|
+
# (non-terminal) record is returned; call {#wait_for_completion} again to
|
|
533
|
+
# keep waiting.
|
|
534
|
+
#
|
|
474
535
|
# @param url [String] seed URL
|
|
475
536
|
# @param max_depth [Integer] maximum crawl depth (default 2)
|
|
476
537
|
# @param max_pages [Integer] maximum pages (default 100)
|
|
538
|
+
# @param wait [Boolean] block until the run is terminal (default false)
|
|
539
|
+
# @param timeout [Integer] total seconds to wait when +wait: true+ (default 300)
|
|
477
540
|
# @param raise_on_result_error [Boolean] raise {Ghostcrawl::ScrapeError} on a
|
|
478
|
-
# target-side (HTTP-200) failure instead of returning the raw hash (default true)
|
|
479
|
-
#
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
541
|
+
# target-side (HTTP-200) failure instead of returning the raw hash (default true).
|
|
542
|
+
# Ignored on the +wait: true+ path, which always returns the terminal run so
|
|
543
|
+
# the caller can inspect a +failed+ status.
|
|
544
|
+
# @return [Hash] crawl run record with +run_id+ and +status+ (results present
|
|
545
|
+
# when it completed while waiting)
|
|
546
|
+
def start(url:, max_depth: 2, max_pages: 100, wait: false, timeout: 300,
|
|
547
|
+
raise_on_result_error: true, **opts)
|
|
548
|
+
data = { "action" => "start", "seed_urls" => [url],
|
|
549
|
+
"max_depth" => max_depth, "max_pages" => max_pages }
|
|
550
|
+
.merge(opts.transform_keys(&:to_s))
|
|
551
|
+
|
|
552
|
+
unless wait
|
|
553
|
+
hash = ResponseHelper.to_hash(@v1.crawl_runs.post(AdditionalDataBody.new(data)))
|
|
554
|
+
return raise_on_result_error ? ResponseHelper.raise_on_result_error!(hash) : hash
|
|
555
|
+
end
|
|
556
|
+
|
|
557
|
+
# Start-and-wait: ask the server to block until terminal. Bound the first
|
|
558
|
+
# server block to a safe window, then long-poll the rest via GET so a low
|
|
559
|
+
# per-request read timeout can never abort the whole wait.
|
|
560
|
+
window = [timeout, WAIT_WINDOW_S].min
|
|
561
|
+
data["wait_until"] = "completed"
|
|
562
|
+
data["timeout_s"] = window
|
|
563
|
+
hash = ResponseHelper.to_hash(@v1.crawl_runs.post(AdditionalDataBody.new(data)))
|
|
564
|
+
return hash if terminal?(hash)
|
|
565
|
+
|
|
566
|
+
run_id = hash["run_id"]
|
|
567
|
+
remaining = timeout - window
|
|
568
|
+
return hash if run_id.nil? || run_id.to_s.empty? || remaining <= 0
|
|
569
|
+
wait_for_completion(run_id, timeout: remaining)
|
|
570
|
+
end
|
|
571
|
+
|
|
572
|
+
# Block until an existing crawl run reaches a terminal state, or +timeout+
|
|
573
|
+
# seconds elapse. Event-driven: each iteration issues a SERVER-blocking
|
|
574
|
+
# +GET /v1/crawl-runs/{run_id}?wait=true&timeout_s=N+ that returns the moment
|
|
575
|
+
# the run goes terminal (or after its own window). There is no client +sleep+
|
|
576
|
+
# — the wait cost lives on the server, and successive blocking windows are
|
|
577
|
+
# chained until the caller's +timeout+ deadline.
|
|
578
|
+
#
|
|
579
|
+
# On timeout the current non-terminal run is returned (never raises for a
|
|
580
|
+
# slow run); a terminal run is returned as soon as it is observed.
|
|
581
|
+
#
|
|
582
|
+
# @param run_id [String] the run to wait on
|
|
583
|
+
# @param timeout [Integer] total seconds to wait (default 300)
|
|
584
|
+
# @return [Hash] the run record (terminal when it finished in time)
|
|
585
|
+
def wait_for_completion(run_id, timeout: 300)
|
|
586
|
+
if run_id.nil? || run_id.to_s.empty?
|
|
587
|
+
raise ArgumentError, "run_id is required"
|
|
588
|
+
end
|
|
589
|
+
|
|
590
|
+
deadline = monotonic + timeout
|
|
591
|
+
loop do
|
|
592
|
+
remaining = deadline - monotonic
|
|
593
|
+
# Deadline reached — one final non-blocking read of the current state.
|
|
594
|
+
return get(run_id) if remaining <= 0
|
|
595
|
+
|
|
596
|
+
window = [remaining, WAIT_WINDOW_S].min
|
|
597
|
+
run = fetch_waiting(run_id, window)
|
|
598
|
+
return run if terminal?(run)
|
|
599
|
+
# The server just blocked for ~window seconds; loop straight into the
|
|
600
|
+
# next blocking window. No client-side delay.
|
|
601
|
+
end
|
|
486
602
|
end
|
|
487
603
|
|
|
488
604
|
# List crawl runs.
|
|
@@ -502,6 +618,33 @@ module Ghostcrawl
|
|
|
502
618
|
def cancel(run_id)
|
|
503
619
|
ResponseHelper.to_hash(@v1.crawl_runs.by_run_id(run_id).cancel.post)
|
|
504
620
|
end
|
|
621
|
+
|
|
622
|
+
private
|
|
623
|
+
|
|
624
|
+
# True when +run+ is a Hash carrying a terminal +status+.
|
|
625
|
+
def terminal?(run)
|
|
626
|
+
run.is_a?(Hash) && TERMINAL_STATUSES.include?(run["status"].to_s)
|
|
627
|
+
end
|
|
628
|
+
|
|
629
|
+
# Issues one SERVER-blocking GET on the run with +?wait=true&timeout_s=N+.
|
|
630
|
+
#
|
|
631
|
+
# The generated item builder's +to_get_request_information+ does not wire
|
|
632
|
+
# query parameters (its URL template has no query placeholder), so we take
|
|
633
|
+
# the request it builds, extend the template with +{?wait,timeout_s}+, and
|
|
634
|
+
# set the params before sending through the raw adapter — mirroring how
|
|
635
|
+
# {Client#me} re-issues a request with the Binary (raw-JSON) response factory.
|
|
636
|
+
def fetch_waiting(run_id, timeout_s)
|
|
637
|
+
request_info = @v1.crawl_runs.by_run_id(run_id).to_get_request_information(nil)
|
|
638
|
+
request_info.url_template = "{+baseurl}/v1/crawl-runs/{run_id}{?wait,timeout_s}"
|
|
639
|
+
request_info.query_parameters["wait"] = "true"
|
|
640
|
+
request_info.query_parameters["timeout_s"] = timeout_s.to_i.to_s
|
|
641
|
+
ResponseHelper.to_hash(@adapter.send_async(request_info, Ghostcrawl::V1::Binary, {}))
|
|
642
|
+
end
|
|
643
|
+
|
|
644
|
+
# Monotonic clock (seconds) — immune to wall-clock adjustments during a wait.
|
|
645
|
+
def monotonic
|
|
646
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
647
|
+
end
|
|
505
648
|
end
|
|
506
649
|
|
|
507
650
|
# Manage browser sessions — /v1/sessions.
|
|
@@ -848,7 +991,9 @@ module Ghostcrawl
|
|
|
848
991
|
# (Binary) response factory instead of the typed MeResponse model.
|
|
849
992
|
@adapter = adapter
|
|
850
993
|
|
|
851
|
-
|
|
994
|
+
# CrawlRunsClient needs the raw adapter for the event-driven long-poll wait
|
|
995
|
+
# (the generated item builder can't attach ?wait/&timeout_s query params).
|
|
996
|
+
@crawl_runs = CrawlRunsClient.new(@v1, @adapter)
|
|
852
997
|
@sessions = SessionsClient.new(@v1)
|
|
853
998
|
# Sub-clients with void-DELETE endpoints (204 No Content) also need the raw
|
|
854
999
|
# adapter so ResponseHelper.void_request! can work around the broken generated
|
|
@@ -945,17 +1090,34 @@ module Ghostcrawl
|
|
|
945
1090
|
|
|
946
1091
|
# Start a deep crawl from a seed URL.
|
|
947
1092
|
# Delegates to POST /v1/crawl/deep via the generated CrawlDeepRequestBuilder.
|
|
1093
|
+
#
|
|
1094
|
+
# Pass +wait: true+ to block until the run is terminal. The deep-crawl start
|
|
1095
|
+
# returns a +run_id+, then the wait is handled by the same event-driven
|
|
1096
|
+
# server-blocking long-poll as {CrawlRunsClient#wait_for_completion} — no
|
|
1097
|
+
# client-side +sleep+ loop.
|
|
1098
|
+
#
|
|
948
1099
|
# @param url [String] seed URL
|
|
949
1100
|
# @param max_depth [Integer] maximum crawl depth (default 2)
|
|
950
1101
|
# @param max_pages [Integer] maximum pages (default 100)
|
|
1102
|
+
# @param wait [Boolean] block until the run is terminal (default false)
|
|
1103
|
+
# @param timeout [Integer] total seconds to wait when +wait: true+ (default 300)
|
|
951
1104
|
# @param raise_on_result_error [Boolean] raise {Ghostcrawl::ScrapeError} on a
|
|
952
|
-
# target-side (HTTP-200) failure instead of returning the raw hash (default true)
|
|
953
|
-
#
|
|
954
|
-
|
|
1105
|
+
# target-side (HTTP-200) failure instead of returning the raw hash (default true).
|
|
1106
|
+
# Ignored on the +wait: true+ path, which returns the terminal run.
|
|
1107
|
+
# @return [Hash] crawl run record (terminal when +wait: true+ and it finished in time)
|
|
1108
|
+
def crawl(url:, max_depth: 2, max_pages: 100, wait: false, timeout: 300,
|
|
1109
|
+
raise_on_result_error: true, **opts)
|
|
955
1110
|
data = { "seed_urls" => [url], "max_depth" => max_depth,
|
|
956
1111
|
"max_urls" => max_pages }.merge(opts.transform_keys(&:to_s))
|
|
957
1112
|
hash = ResponseHelper.to_hash(@v1.crawl.deep.post(AdditionalDataBody.new(data)))
|
|
958
|
-
|
|
1113
|
+
|
|
1114
|
+
unless wait
|
|
1115
|
+
return raise_on_result_error ? ResponseHelper.raise_on_result_error!(hash) : hash
|
|
1116
|
+
end
|
|
1117
|
+
|
|
1118
|
+
run_id = hash["run_id"]
|
|
1119
|
+
return hash if run_id.nil? || run_id.to_s.empty?
|
|
1120
|
+
@crawl_runs.wait_for_completion(run_id, timeout: timeout)
|
|
959
1121
|
end
|
|
960
1122
|
|
|
961
1123
|
# Map all URLs reachable from a seed URL.
|
|
@@ -982,18 +1144,211 @@ module Ghostcrawl
|
|
|
982
1144
|
ResponseHelper.to_hash(@adapter.send_async(request_info, Ghostcrawl::V1::Binary, {}))
|
|
983
1145
|
end
|
|
984
1146
|
|
|
1147
|
+
# Render a URL to a PDF document and return the raw +application/pdf+ bytes.
|
|
1148
|
+
# Delegates to POST /v1/pdf.
|
|
1149
|
+
#
|
|
1150
|
+
# +/v1/pdf+ responds with binary, not a JSON envelope, so this returns the
|
|
1151
|
+
# bytes verbatim (an ASCII-8BIT String) — write them straight to a file:
|
|
1152
|
+
#
|
|
1153
|
+
# data = client.pdf(url: "https://example.com")
|
|
1154
|
+
# File.binwrite("page.pdf", data)
|
|
1155
|
+
#
|
|
1156
|
+
# PDF output is Chrome-only; a request that resolves to a Firefox or WebKit
|
|
1157
|
+
# identity is rejected with 400 +pdf_engine_unsupported+ (a {GhostcrawlError}).
|
|
1158
|
+
#
|
|
1159
|
+
# +/v1/pdf+ has no generated request builder, so this hand-builds a
|
|
1160
|
+
# {MicrosoftKiotaAbstractions::RequestInformation} and routes it through the
|
|
1161
|
+
# SAME adapter (base URL + bearer auth + transport) as the modeled calls.
|
|
1162
|
+
#
|
|
1163
|
+
# @param url [String] target URL to render
|
|
1164
|
+
# @param paper_format [String] page size: "a4" (default), "letter", "legal", "tabloid"
|
|
1165
|
+
# @param landscape [Boolean] render in landscape orientation (default false)
|
|
1166
|
+
# @param engine [String] browser engine (PDF is Chrome-only; default "auto")
|
|
1167
|
+
# @return [String] the raw PDF bytes (ASCII-8BIT)
|
|
1168
|
+
def pdf(url:, paper_format: "a4", landscape: false, engine: "auto", **opts)
|
|
1169
|
+
data = { "url" => url, "paper_format" => paper_format,
|
|
1170
|
+
"landscape" => landscape, "engine" => engine }.merge(opts.transform_keys(&:to_s))
|
|
1171
|
+
request_info = MicrosoftKiotaAbstractions::RequestInformation.new
|
|
1172
|
+
request_info.http_method = :POST
|
|
1173
|
+
request_info.url_template = "{+baseurl}/v1/pdf"
|
|
1174
|
+
request_info.headers.try_add("Accept", "application/pdf")
|
|
1175
|
+
request_info.set_stream_content(JSON.generate(data), "application/json")
|
|
1176
|
+
ResponseHelper.binary_request!(@adapter, request_info)
|
|
1177
|
+
end
|
|
1178
|
+
|
|
1179
|
+
# Capture a screenshot of a URL and return the raw +image/png+ bytes.
|
|
1180
|
+
# Delegates to POST /v1/screenshot.
|
|
1181
|
+
#
|
|
1182
|
+
# +/v1/screenshot+ responds with a binary image, not a JSON envelope, so this
|
|
1183
|
+
# returns the bytes verbatim (an ASCII-8BIT String) — write them straight to a
|
|
1184
|
+
# file:
|
|
1185
|
+
#
|
|
1186
|
+
# data = client.screenshot(url: "https://example.com")
|
|
1187
|
+
# File.binwrite("page.png", data)
|
|
1188
|
+
#
|
|
1189
|
+
# +/v1/screenshot+ has no generated request builder, so this hand-builds a
|
|
1190
|
+
# {MicrosoftKiotaAbstractions::RequestInformation} and routes it through the
|
|
1191
|
+
# SAME adapter (base URL + bearer auth + transport) as the modeled calls,
|
|
1192
|
+
# mirroring {#pdf} exactly (the raw-bytes dispatch idiom).
|
|
1193
|
+
#
|
|
1194
|
+
# @param url [String] target URL to capture
|
|
1195
|
+
# @param format [String] image format: "png" (default), "jpeg", "webp"
|
|
1196
|
+
# @param full_page [Boolean] capture the full scrollable page (default false)
|
|
1197
|
+
# @param screenshot_selector [String, nil] CSS selector to clip to (optional)
|
|
1198
|
+
# @param engine [String] browser engine (default "auto")
|
|
1199
|
+
# @return [String] the raw image bytes (ASCII-8BIT)
|
|
1200
|
+
def screenshot(url:, format: "png", full_page: false, screenshot_selector: nil,
|
|
1201
|
+
engine: "auto", **opts)
|
|
1202
|
+
data = { "url" => url, "format" => format, "full_page" => full_page,
|
|
1203
|
+
"engine" => engine }
|
|
1204
|
+
data["screenshot_selector"] = screenshot_selector unless screenshot_selector.nil?
|
|
1205
|
+
data.merge!(opts.transform_keys(&:to_s))
|
|
1206
|
+
request_info = MicrosoftKiotaAbstractions::RequestInformation.new
|
|
1207
|
+
request_info.http_method = :POST
|
|
1208
|
+
request_info.url_template = "{+baseurl}/v1/screenshot"
|
|
1209
|
+
request_info.headers.try_add("Accept", "image/png")
|
|
1210
|
+
request_info.set_stream_content(JSON.generate(data), "application/json")
|
|
1211
|
+
ResponseHelper.binary_request!(@adapter, request_info)
|
|
1212
|
+
end
|
|
1213
|
+
|
|
1214
|
+
# Render a URL and return the rendered-content JSON envelope as a Hash.
|
|
1215
|
+
# Delegates to POST /v1/content.
|
|
1216
|
+
#
|
|
1217
|
+
# +/v1/content+ responds with +application/json+ ({+content+, +url+, +status+,
|
|
1218
|
+
# +format+, +status_code+, +bytes+}); this returns the decoded Hash. It uses
|
|
1219
|
+
# the same hand-built {MicrosoftKiotaAbstractions::RequestInformation} +
|
|
1220
|
+
# shared adapter dispatch as {#pdf}, parsing the JSON body at the end.
|
|
1221
|
+
#
|
|
1222
|
+
# @param url [String] target URL to render
|
|
1223
|
+
# @param engine [String] browser engine (default "auto")
|
|
1224
|
+
# @return [Hash] the rendered-content envelope (top-level +content+ = HTML)
|
|
1225
|
+
def content(url:, engine: "auto", **opts)
|
|
1226
|
+
data = { "url" => url, "engine" => engine }.merge(opts.transform_keys(&:to_s))
|
|
1227
|
+
request_info = MicrosoftKiotaAbstractions::RequestInformation.new
|
|
1228
|
+
request_info.http_method = :POST
|
|
1229
|
+
request_info.url_template = "{+baseurl}/v1/content"
|
|
1230
|
+
request_info.headers.try_add("Accept", "application/json")
|
|
1231
|
+
request_info.set_stream_content(JSON.generate(data), "application/json")
|
|
1232
|
+
bytes = ResponseHelper.binary_request!(@adapter, request_info)
|
|
1233
|
+
parsed = JSON.parse(bytes)
|
|
1234
|
+
parsed.is_a?(Hash) ? parsed : { "content" => parsed }
|
|
1235
|
+
end
|
|
1236
|
+
|
|
1237
|
+
# Execute an agent task via POST /v1/agent.
|
|
1238
|
+
#
|
|
1239
|
+
# The agent capability is gated per account; when it is not enabled the API
|
|
1240
|
+
# replies +404 not_found+ — this returns that problem+json body as a Hash
|
|
1241
|
+
# (carrying +"detail"+) rather than raising, so callers can branch on
|
|
1242
|
+
# +result.key?("detail")+. The LLM step is BYO / not a hosted agent loop
|
|
1243
|
+
# (RESEARCH §2) — this method only proves the client serialize→POST→parse
|
|
1244
|
+
# plumbing round-trips against the real route.
|
|
1245
|
+
#
|
|
1246
|
+
# +/v1/agent+ has no generated request builder, so this hand-builds a
|
|
1247
|
+
# {MicrosoftKiotaAbstractions::RequestInformation} and routes it through the
|
|
1248
|
+
# SAME adapter (base URL + bearer auth + transport) as the modeled calls.
|
|
1249
|
+
#
|
|
1250
|
+
# @param url [String, nil] optional starting URL (folded into +task+ as +start_url+)
|
|
1251
|
+
# @param instruction [String, nil] natural-language task instruction
|
|
1252
|
+
# @param task [Hash, nil] explicit structured task object (overrides url/instruction)
|
|
1253
|
+
# @return [Hash] the agent result, or the gated-404 problem+json body (has +"detail"+)
|
|
1254
|
+
def agent(url: nil, instruction: nil, task: nil, **opts)
|
|
1255
|
+
payload = task || { "instruction" => instruction, "start_url" => url }
|
|
1256
|
+
data = { "task" => payload }.merge(opts.transform_keys(&:to_s))
|
|
1257
|
+
request_info = MicrosoftKiotaAbstractions::RequestInformation.new
|
|
1258
|
+
request_info.http_method = :POST
|
|
1259
|
+
request_info.url_template = "{+baseurl}/v1/agent"
|
|
1260
|
+
request_info.headers.try_add("Accept", "application/json")
|
|
1261
|
+
request_info.set_stream_content(JSON.generate(data), "application/json")
|
|
1262
|
+
bytes = ResponseHelper.binary_request!(@adapter, request_info)
|
|
1263
|
+
parsed = JSON.parse(bytes)
|
|
1264
|
+
parsed.is_a?(Hash) ? parsed : { "detail" => parsed }
|
|
1265
|
+
rescue Ghostcrawl::GhostcrawlError => e
|
|
1266
|
+
# Agent is account-gated: a 404 not_found is the documented "capability
|
|
1267
|
+
# disabled" answer, not a transport failure. Surface the problem+json body
|
|
1268
|
+
# as a Hash (carrying "detail") rather than raising. Any other status is a
|
|
1269
|
+
# real error and is re-raised.
|
|
1270
|
+
raise unless e.status_code == 404
|
|
1271
|
+
body = e.body.to_s
|
|
1272
|
+
brace = body.index("{")
|
|
1273
|
+
decoded =
|
|
1274
|
+
if brace
|
|
1275
|
+
begin
|
|
1276
|
+
JSON.parse(body[brace..])
|
|
1277
|
+
rescue StandardError
|
|
1278
|
+
nil
|
|
1279
|
+
end
|
|
1280
|
+
end
|
|
1281
|
+
decoded.is_a?(Hash) ? decoded : { "detail" => e.message, "code" => (e.code || "not_found") }
|
|
1282
|
+
end
|
|
1283
|
+
|
|
1284
|
+
# Create a new identity envelope.
|
|
1285
|
+
# Delegates to POST /v1/identity. Returns the opaque encrypted identity
|
|
1286
|
+
# envelope; the SDK never decodes or inspects the ciphertext.
|
|
1287
|
+
#
|
|
1288
|
+
# @param claim_os [String] OS to emulate (ios/android/macos/windows/linux)
|
|
1289
|
+
# @param claim_browser [String] browser to emulate (chrome/firefox/safari)
|
|
1290
|
+
# @param device_model [String, nil] device model (required for mobile claim_os)
|
|
1291
|
+
# @param viewport [Object, nil] "fullscreen"/"any" or {"width"=>Integer,"height"=>Integer}
|
|
1292
|
+
# @return [Hash] the identity envelope
|
|
1293
|
+
def identity(claim_os:, claim_browser:, device_model: nil, viewport: nil, **opts)
|
|
1294
|
+
data = { "claim_os" => claim_os, "claim_browser" => claim_browser }
|
|
1295
|
+
data["device_model"] = device_model unless device_model.nil?
|
|
1296
|
+
data["viewport"] = viewport unless viewport.nil?
|
|
1297
|
+
data.merge!(opts.transform_keys(&:to_s))
|
|
1298
|
+
json_request(:POST, "/v1/identity", data)
|
|
1299
|
+
end
|
|
1300
|
+
|
|
1301
|
+
# Get the current account's cost/usage report.
|
|
1302
|
+
# Delegates to GET /v1/me/usage (the canonical account-scoped usage endpoint).
|
|
1303
|
+
# @return [Hash] usage envelope (+wallet+, +total_usd+, +by_engine+, …)
|
|
1304
|
+
def usage
|
|
1305
|
+
json_request(:GET, "/v1/me/usage", nil)
|
|
1306
|
+
end
|
|
1307
|
+
|
|
1308
|
+
# List the account's persisted session storage-states.
|
|
1309
|
+
# Delegates to GET /v1/storage-states.
|
|
1310
|
+
# @return [Hash] the storage-states envelope
|
|
1311
|
+
def storage_states
|
|
1312
|
+
json_request(:GET, "/v1/storage-states", nil)
|
|
1313
|
+
end
|
|
1314
|
+
|
|
985
1315
|
private
|
|
986
1316
|
|
|
1317
|
+
# Route an ad-hoc JSON request through the SAME adapter (base URL + bearer
|
|
1318
|
+
# auth + transport) as the modeled calls, for routes the generated core has
|
|
1319
|
+
# no builder for. Returns the decoded Hash. Mirrors {#content}'s dispatch.
|
|
1320
|
+
# @api private
|
|
1321
|
+
def json_request(method, path, body)
|
|
1322
|
+
request_info = MicrosoftKiotaAbstractions::RequestInformation.new
|
|
1323
|
+
request_info.http_method = method
|
|
1324
|
+
request_info.url_template = "{+baseurl}#{path}"
|
|
1325
|
+
request_info.headers.try_add("Accept", "application/json")
|
|
1326
|
+
if !body.nil? && method != :GET
|
|
1327
|
+
request_info.set_stream_content(JSON.generate(body), "application/json")
|
|
1328
|
+
end
|
|
1329
|
+
bytes = ResponseHelper.binary_request!(@adapter, request_info)
|
|
1330
|
+
parsed = (JSON.parse(bytes) if bytes && !bytes.empty?)
|
|
1331
|
+
parsed.is_a?(Hash) ? parsed : {}
|
|
1332
|
+
end
|
|
1333
|
+
|
|
987
1334
|
# Normalizes a +"content"+ key onto a decoded scrape response.
|
|
988
1335
|
#
|
|
989
|
-
# The API
|
|
990
|
-
#
|
|
991
|
-
#
|
|
992
|
-
#
|
|
1336
|
+
# The API has two scrape response shapes:
|
|
1337
|
+
# * the FLAT +format="markdown"+ shape, which carries the rendered page at
|
|
1338
|
+
# the top level under the format-specific key (+"markdown"+/+"html"+/
|
|
1339
|
+
# +"text"+) but omits +identity_id+; and
|
|
1340
|
+
# * the standard ENVELOPE ({ +status+, +results+, +routing_mode+,
|
|
1341
|
+
# +request_class+, +identity_id+ }), where the rendered page lives under
|
|
1342
|
+
# +results[0]+ and the top-level +identity_id+ is present.
|
|
1343
|
+
# The documented quickstart reads +result["content"]+, so this mirrors the
|
|
1344
|
+
# rendered page onto +"content"+ in place from WHICHEVER shape is present,
|
|
1345
|
+
# KEEPING the original keys intact (backward compatible).
|
|
993
1346
|
#
|
|
994
1347
|
# No-op unless +result+ is a Hash that does not already carry +"content"+.
|
|
995
1348
|
# The value chosen is: the field named by +result["format"]+ when that field
|
|
996
|
-
# is a String, else the first String among +"markdown"+, +"html"+, +"text"
|
|
1349
|
+
# is a String, else the first String among +"markdown"+, +"html"+, +"text"+ —
|
|
1350
|
+
# looked up first at the top level (flat shape), then on +results[0]+
|
|
1351
|
+
# (envelope shape).
|
|
997
1352
|
#
|
|
998
1353
|
# @param result [Object] the decoded response (only mutated when a Hash)
|
|
999
1354
|
# @return [Object] the same +result+, unchanged reference
|
|
@@ -1002,21 +1357,34 @@ module Ghostcrawl
|
|
|
1002
1357
|
return result unless result.is_a?(Hash) && !result.key?("content")
|
|
1003
1358
|
|
|
1004
1359
|
fmt = result["format"]
|
|
1005
|
-
value = result
|
|
1006
|
-
value = nil unless value.is_a?(String)
|
|
1360
|
+
value = scrape_content_from(result, fmt)
|
|
1007
1361
|
|
|
1362
|
+
# Envelope shape: the page is under results[0], not at the top level.
|
|
1008
1363
|
unless value.is_a?(String)
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
value = candidate
|
|
1013
|
-
break
|
|
1014
|
-
end
|
|
1015
|
-
end
|
|
1364
|
+
first = result["results"]
|
|
1365
|
+
first = first.first if first.is_a?(Array)
|
|
1366
|
+
value = scrape_content_from(first, fmt) if first.is_a?(Hash)
|
|
1016
1367
|
end
|
|
1017
1368
|
|
|
1018
1369
|
result["content"] = value if value.is_a?(String)
|
|
1019
1370
|
result
|
|
1020
1371
|
end
|
|
1372
|
+
|
|
1373
|
+
# Picks the rendered-page String out of a scrape result Hash: the field named
|
|
1374
|
+
# by +fmt+ when present, else the first String among +"markdown"+, +"html"+,
|
|
1375
|
+
# +"text"+. Returns +nil+ when +src+ carries no such String.
|
|
1376
|
+
# @api private
|
|
1377
|
+
def scrape_content_from(src, fmt)
|
|
1378
|
+
return nil unless src.is_a?(Hash)
|
|
1379
|
+
|
|
1380
|
+
value = src[fmt] if fmt.is_a?(String)
|
|
1381
|
+
return value if value.is_a?(String)
|
|
1382
|
+
|
|
1383
|
+
%w[markdown html text].each do |key|
|
|
1384
|
+
candidate = src[key]
|
|
1385
|
+
return candidate if candidate.is_a?(String)
|
|
1386
|
+
end
|
|
1387
|
+
nil
|
|
1388
|
+
end
|
|
1021
1389
|
end
|
|
1022
1390
|
end
|
data/lib/ghostcrawl/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ghostcrawl
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.
|
|
4
|
+
version: 2.3.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- GhostCrawl
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-07-
|
|
11
|
+
date: 2026-07-21 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: microsoft_kiota_abstractions
|
|
@@ -225,9 +225,6 @@ files:
|
|
|
225
225
|
- _generated/v1/billing/usage/usage.rb
|
|
226
226
|
- _generated/v1/billing/usage/usage_request_builder.rb
|
|
227
227
|
- _generated/v1/binary/binary.rb
|
|
228
|
-
- _generated/v1/binary/binary_request_builder.rb
|
|
229
|
-
- _generated/v1/binary/download/download.rb
|
|
230
|
-
- _generated/v1/binary/download/download_request_builder.rb
|
|
231
228
|
- _generated/v1/budgets/budgets.rb
|
|
232
229
|
- _generated/v1/budgets/budgets_request_builder.rb
|
|
233
230
|
- _generated/v1/budgets/policy/item/item.rb
|
|
@@ -431,7 +428,7 @@ files:
|
|
|
431
428
|
- lib/ghostcrawl/version.rb
|
|
432
429
|
homepage: https://ghostcrawl.io
|
|
433
430
|
licenses:
|
|
434
|
-
-
|
|
431
|
+
- MIT
|
|
435
432
|
metadata:
|
|
436
433
|
homepage_uri: https://ghostcrawl.io
|
|
437
434
|
documentation_uri: https://docs.ghostcrawl.io
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
require 'microsoft_kiota_abstractions'
|
|
2
|
-
require_relative '../../ghostcrawl'
|
|
3
|
-
require_relative '../v1'
|
|
4
|
-
require_relative './binary'
|
|
5
|
-
require_relative './download/download_request_builder'
|
|
6
|
-
|
|
7
|
-
module Ghostcrawl
|
|
8
|
-
module V1
|
|
9
|
-
module Binary
|
|
10
|
-
##
|
|
11
|
-
# Builds and executes requests for operations under #v1#binary
|
|
12
|
-
class BinaryRequestBuilder < MicrosoftKiotaAbstractions::BaseRequestBuilder
|
|
13
|
-
|
|
14
|
-
##
|
|
15
|
-
# The download property
|
|
16
|
-
def download()
|
|
17
|
-
return Ghostcrawl::V1::Binary::Download::DownloadRequestBuilder.new(@path_parameters, @request_adapter)
|
|
18
|
-
end
|
|
19
|
-
##
|
|
20
|
-
## Instantiates a new BinaryRequestBuilder and sets the default values.
|
|
21
|
-
## @param path_parameters Path parameters for the request
|
|
22
|
-
## @param request_adapter The request adapter to use to execute the requests.
|
|
23
|
-
## @return a void
|
|
24
|
-
##
|
|
25
|
-
def initialize(path_parameters, request_adapter)
|
|
26
|
-
super(path_parameters, request_adapter, "{+baseurl}/v1/binary")
|
|
27
|
-
end
|
|
28
|
-
end
|
|
29
|
-
end
|
|
30
|
-
end
|
|
31
|
-
end
|
|
File without changes
|
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
require 'microsoft_kiota_abstractions'
|
|
2
|
-
require_relative '../../../ghostcrawl'
|
|
3
|
-
require_relative '../../../models/h_t_t_p_validation_error'
|
|
4
|
-
require_relative '../../v1'
|
|
5
|
-
require_relative '../binary'
|
|
6
|
-
require_relative './download'
|
|
7
|
-
|
|
8
|
-
module Ghostcrawl
|
|
9
|
-
module V1
|
|
10
|
-
module Binary
|
|
11
|
-
module Download
|
|
12
|
-
##
|
|
13
|
-
# Builds and executes requests for operations under #v1#binary#download
|
|
14
|
-
class DownloadRequestBuilder < MicrosoftKiotaAbstractions::BaseRequestBuilder
|
|
15
|
-
|
|
16
|
-
##
|
|
17
|
-
## Instantiates a new DownloadRequestBuilder and sets the default values.
|
|
18
|
-
## @param path_parameters Path parameters for the request
|
|
19
|
-
## @param request_adapter The request adapter to use to execute the requests.
|
|
20
|
-
## @return a void
|
|
21
|
-
##
|
|
22
|
-
def initialize(path_parameters, request_adapter)
|
|
23
|
-
super(path_parameters, request_adapter, "{+baseurl}/v1/binary/download?engine={engine}{&release_id*}")
|
|
24
|
-
end
|
|
25
|
-
##
|
|
26
|
-
## Download your licensed engine binary (Chrome, Firefox, or WebKit) for self-hosting.Select the engine via the ``engine`` query parameter. The binary is streamedto the client and bound to your account.
|
|
27
|
-
## @param request_configuration Configuration for the request such as headers, query parameters, and middleware options.
|
|
28
|
-
## @return a Fiber of binary
|
|
29
|
-
##
|
|
30
|
-
def get(request_configuration=nil)
|
|
31
|
-
request_info = self.to_get_request_information(
|
|
32
|
-
request_configuration
|
|
33
|
-
)
|
|
34
|
-
error_mapping = Hash.new
|
|
35
|
-
error_mapping["422"] = lambda {|pn| Ghostcrawl::Models::HTTPValidationError.create_from_discriminator_value(pn) }
|
|
36
|
-
return @request_adapter.send_async(request_info, Binary, error_mapping)
|
|
37
|
-
end
|
|
38
|
-
##
|
|
39
|
-
## Download your licensed engine binary (Chrome, Firefox, or WebKit) for self-hosting.Select the engine via the ``engine`` query parameter. The binary is streamedto the client and bound to your account.
|
|
40
|
-
## @param request_configuration Configuration for the request such as headers, query parameters, and middleware options.
|
|
41
|
-
## @return a request_information
|
|
42
|
-
##
|
|
43
|
-
def to_get_request_information(request_configuration=nil)
|
|
44
|
-
request_info = MicrosoftKiotaAbstractions::RequestInformation.new()
|
|
45
|
-
unless request_configuration.nil?
|
|
46
|
-
request_info.add_headers_from_raw_object(request_configuration.headers)
|
|
47
|
-
request_info.set_query_string_parameters_from_raw_object(request_configuration.query_parameters)
|
|
48
|
-
request_info.add_request_options(request_configuration.options)
|
|
49
|
-
end
|
|
50
|
-
request_info.url_template = @url_template
|
|
51
|
-
request_info.path_parameters = @path_parameters
|
|
52
|
-
request_info.http_method = :GET
|
|
53
|
-
request_info.headers.try_add('Accept', 'application/json')
|
|
54
|
-
return request_info
|
|
55
|
-
end
|
|
56
|
-
##
|
|
57
|
-
## Returns a request builder with the provided arbitrary URL. Using this method means any other path or query parameters are ignored.
|
|
58
|
-
## @param raw_url The raw URL to use for the request builder.
|
|
59
|
-
## @return a download_request_builder
|
|
60
|
-
##
|
|
61
|
-
def with_url(raw_url)
|
|
62
|
-
raise StandardError, 'raw_url cannot be null' if raw_url.nil?
|
|
63
|
-
return DownloadRequestBuilder.new(raw_url, @request_adapter)
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
##
|
|
67
|
-
# Download your licensed engine binary (Chrome, Firefox, or WebKit) for self-hosting.Select the engine via the ``engine`` query parameter. The binary is streamedto the client and bound to your account.
|
|
68
|
-
class DownloadRequestBuilderGetQueryParameters
|
|
69
|
-
|
|
70
|
-
attr_accessor :engine
|
|
71
|
-
attr_accessor :release_id
|
|
72
|
-
end
|
|
73
|
-
end
|
|
74
|
-
end
|
|
75
|
-
end
|
|
76
|
-
end
|
|
77
|
-
end
|