riveter-sdk 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +95 -0
- data/lib/riveter/client.rb +144 -0
- data/lib/riveter/errors.rb +61 -0
- data/lib/riveter/models.rb +122 -0
- data/lib/riveter/page.rb +43 -0
- data/lib/riveter/resources.rb +175 -0
- data/lib/riveter/version.rb +5 -0
- data/lib/riveter.rb +12 -0
- metadata +52 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 908379b467d0f1129dc3794df97bd927aeb3cdbdaaadd3146fe96c887f2835b9
|
|
4
|
+
data.tar.gz: a23f012aaad173284d2759f1754de5bc5cf50d1225e437445774e3bc723245bc
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 38b1aac5504c3eac8c6256a0e559c02d4b5e526fb9a55de1d41214eeb0306be245c4dc991abbc17600fdabf11fc66dd96fb94383a23937575330c8c76bebcdb5
|
|
7
|
+
data.tar.gz: a3e028f0a9e5db8842770b970c94a77b1525b0480b1564ca3e63dd30804e1a45eb2c9eabc0aee475b3496b35394ae599398d6965bc8afbd7fcaaa5c98f72818b
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Cody
|
|
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
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# riveter-sdk
|
|
2
|
+
|
|
3
|
+
Official Ruby SDK for the [Riveter API](https://docs.riveterhq.com) — enrich data,
|
|
4
|
+
build datasets, scrape pages, and run web searches.
|
|
5
|
+
|
|
6
|
+
Requires Ruby 3.1+. Zero runtime dependencies (stdlib `Net::HTTP`).
|
|
7
|
+
|
|
8
|
+
## Install
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
gem install riveter-sdk
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Or in a Gemfile:
|
|
15
|
+
|
|
16
|
+
```ruby
|
|
17
|
+
gem "riveter-sdk", require: "riveter"
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Quickstart
|
|
21
|
+
|
|
22
|
+
```ruby
|
|
23
|
+
require "riveter"
|
|
24
|
+
|
|
25
|
+
# Reads ENV["RIVETER_API_KEY"] when api_key: is not passed.
|
|
26
|
+
# Get a key at https://app.riveterhq.com/settings/api
|
|
27
|
+
riveter = Riveter::Client.new(api_key: "YOUR_API_KEY")
|
|
28
|
+
|
|
29
|
+
run = riveter.enrich(
|
|
30
|
+
prompt: "Research each company",
|
|
31
|
+
attributes: ["CEO", "Employee Count"],
|
|
32
|
+
input: { "Company" => ["Apple", "Google"] }
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
result = riveter.runs.wait_for_result(run.id)
|
|
36
|
+
puts result.output
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## The run lifecycle
|
|
40
|
+
|
|
41
|
+
Every async kickoff (`enrich`, `datasets.build`, `extractions.run`, ...) returns a run.
|
|
42
|
+
|
|
43
|
+
```ruby
|
|
44
|
+
riveter.runs.get(run.id) # status + progress
|
|
45
|
+
riveter.runs.result(run.id, wait: 50) # output (long-polls up to 50s)
|
|
46
|
+
riveter.runs.wait_for_result(run.id, timeout: 600) # poll until finished
|
|
47
|
+
riveter.runs.stop(run.id) # stop early
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
List runs with automatic pagination:
|
|
51
|
+
|
|
52
|
+
```ruby
|
|
53
|
+
riveter.runs.list(status: "success").auto_paging_each do |run|
|
|
54
|
+
puts run.id
|
|
55
|
+
end
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Surface
|
|
59
|
+
|
|
60
|
+
- `riveter.enrich(...)`, `riveter.quick_search(...)`, `riveter.scrape(...)`, `riveter.account`
|
|
61
|
+
- `riveter.runs` — `get`, `result`, `stop`, `list`, `summary`, `wait_for_result`
|
|
62
|
+
- `riveter.enrichments` — `list`, `create`, `get`, `update`, `build_dataset`
|
|
63
|
+
- `riveter.datasets` — `build`, `extend_dataset`
|
|
64
|
+
- `riveter.configured_datasets` — `build`
|
|
65
|
+
- `riveter.extractions` — `create`, `get`, `run`
|
|
66
|
+
- `riveter.monitors` — `create`, `list`, `get`, `update`, `runs`
|
|
67
|
+
|
|
68
|
+
Responses are lightweight model objects; fields the SDK does not know yet stay reachable
|
|
69
|
+
via `#raw` / `#[]`.
|
|
70
|
+
|
|
71
|
+
## Errors and retries
|
|
72
|
+
|
|
73
|
+
API failures raise `Riveter::APIError` with `#status`, `#type` (`not_found`,
|
|
74
|
+
`insufficient_credits`, ...), `#message`, and optional `#details`. Network failures raise
|
|
75
|
+
`Riveter::APIConnectionError` / `Riveter::APITimeoutError`. 429s are retried automatically
|
|
76
|
+
using the `X-RateLimit-Reset` header; 5xx and network failures are retried for GETs.
|
|
77
|
+
Configure with `max_retries:` and `timeout:`.
|
|
78
|
+
|
|
79
|
+
## Options
|
|
80
|
+
|
|
81
|
+
```ruby
|
|
82
|
+
Riveter::Client.new(
|
|
83
|
+
api_key: "...", # default: ENV["RIVETER_API_KEY"]
|
|
84
|
+
base_url: "https://api.riveterhq.com/v1", # default: ENV["RIVETER_BASE_URL"], then this
|
|
85
|
+
timeout: 60, # keep above 50 for `wait` long-polls
|
|
86
|
+
max_retries: 2
|
|
87
|
+
)
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## Development
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
bundle install
|
|
94
|
+
bundle exec rspec
|
|
95
|
+
```
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "net/http"
|
|
5
|
+
require "openssl"
|
|
6
|
+
require "uri"
|
|
7
|
+
|
|
8
|
+
module Riveter
|
|
9
|
+
# Client for the Riveter API. https://docs.riveterhq.com
|
|
10
|
+
class Client
|
|
11
|
+
DEFAULT_BASE_URL = "https://api.riveterhq.com/v1"
|
|
12
|
+
|
|
13
|
+
attr_reader :base_url, :timeout, :max_retries,
|
|
14
|
+
:runs, :enrichments, :datasets, :configured_datasets,
|
|
15
|
+
:extractions, :monitors
|
|
16
|
+
|
|
17
|
+
# api_key falls back to ENV["RIVETER_API_KEY"]; base_url falls back to
|
|
18
|
+
# ENV["RIVETER_BASE_URL"], then production. timeout (seconds) must exceed
|
|
19
|
+
# the 50s `wait` long-poll. max_retries covers 429s (any method) and
|
|
20
|
+
# 5xx/network failures (GETs only).
|
|
21
|
+
def initialize(api_key: nil, base_url: nil, timeout: 60, max_retries: 2)
|
|
22
|
+
@api_key = api_key || ENV.fetch("RIVETER_API_KEY", nil)
|
|
23
|
+
if @api_key.nil? || @api_key.empty?
|
|
24
|
+
raise Error, "Missing API key: pass api_key: or set RIVETER_API_KEY " \
|
|
25
|
+
"(get one at https://app.riveterhq.com/settings/api)"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
@base_url = (base_url || ENV.fetch("RIVETER_BASE_URL", nil) || DEFAULT_BASE_URL).sub(%r{/+\z}, "")
|
|
29
|
+
@timeout = timeout
|
|
30
|
+
@max_retries = max_retries
|
|
31
|
+
@runs = Resources::Runs.new(self)
|
|
32
|
+
@enrichments = Resources::Enrichments.new(self)
|
|
33
|
+
@datasets = Resources::Datasets.new(self)
|
|
34
|
+
@configured_datasets = Resources::ConfiguredDatasets.new(self)
|
|
35
|
+
@extractions = Resources::Extractions.new(self)
|
|
36
|
+
@monitors = Resources::Monitors.new(self)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Start an enrichment run on rows of input data. (operationId: enrich)
|
|
40
|
+
def enrich(**params)
|
|
41
|
+
Run.new(request(:post, "/enrich", body: params))
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Run one web search synchronously; results are already in #output. (operationId: quickSearch)
|
|
45
|
+
def quick_search(**params)
|
|
46
|
+
Run.new(request(:post, "/quick_search", body: params))
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Scrape a webpage and return its text synchronously. (operationId: scrape)
|
|
50
|
+
def scrape(**params)
|
|
51
|
+
ScrapeResult.new(request(:post, "/scrape", body: params))
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Account, plan, and credit info for the API key. (operationId: getAccount)
|
|
55
|
+
def account
|
|
56
|
+
AccountInfo.new(request(:get, "/account"))
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def request(method, path, query: nil, body: nil, timeout: nil)
|
|
60
|
+
uri = URI.parse(@base_url + path)
|
|
61
|
+
if query
|
|
62
|
+
compact = query.reject { |_key, value| value.nil? }
|
|
63
|
+
uri.query = URI.encode_www_form(compact) unless compact.empty?
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
retry_on_failure = method == :get
|
|
67
|
+
attempt = 0
|
|
68
|
+
loop do
|
|
69
|
+
response = begin
|
|
70
|
+
perform_http_request(method, uri, body, timeout || @timeout)
|
|
71
|
+
rescue Net::OpenTimeout, Net::ReadTimeout => e
|
|
72
|
+
if retry_on_failure && attempt < @max_retries
|
|
73
|
+
sleep(backoff_seconds(attempt))
|
|
74
|
+
attempt += 1
|
|
75
|
+
next
|
|
76
|
+
end
|
|
77
|
+
raise APITimeoutError, "Request timed out: #{method.to_s.upcase} #{path} (#{e.class})"
|
|
78
|
+
rescue SystemCallError, SocketError, IOError, OpenSSL::SSL::SSLError => e
|
|
79
|
+
if retry_on_failure && attempt < @max_retries
|
|
80
|
+
sleep(backoff_seconds(attempt))
|
|
81
|
+
attempt += 1
|
|
82
|
+
next
|
|
83
|
+
end
|
|
84
|
+
raise APIConnectionError, "Request failed: #{method.to_s.upcase} #{path}: #{e.message}"
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
status = response.code.to_i
|
|
88
|
+
return parse_json_body(response) if status.between?(200, 299)
|
|
89
|
+
|
|
90
|
+
if status == 429 && attempt < @max_retries
|
|
91
|
+
sleep(rate_limit_delay_seconds(response, attempt))
|
|
92
|
+
attempt += 1
|
|
93
|
+
next
|
|
94
|
+
end
|
|
95
|
+
if status >= 500 && retry_on_failure && attempt < @max_retries
|
|
96
|
+
sleep(backoff_seconds(attempt))
|
|
97
|
+
attempt += 1
|
|
98
|
+
next
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
raise APIError.from_response(status, parse_json_body(response))
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
private
|
|
106
|
+
|
|
107
|
+
def perform_http_request(method, uri, body, timeout)
|
|
108
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
109
|
+
http.use_ssl = uri.scheme == "https"
|
|
110
|
+
http.open_timeout = timeout
|
|
111
|
+
http.read_timeout = timeout
|
|
112
|
+
|
|
113
|
+
request_class = { get: Net::HTTP::Get, post: Net::HTTP::Post, patch: Net::HTTP::Patch }.fetch(method)
|
|
114
|
+
request = request_class.new(uri)
|
|
115
|
+
request["Authorization"] = "Bearer #{@api_key}"
|
|
116
|
+
request["User-Agent"] = "riveter-sdk-ruby/#{VERSION}"
|
|
117
|
+
if body
|
|
118
|
+
request["Content-Type"] = "application/json"
|
|
119
|
+
request.body = JSON.generate(body.reject { |_key, value| value.nil? })
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
http.request(request)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def parse_json_body(response)
|
|
126
|
+
return nil if response.body.nil? || response.body.empty?
|
|
127
|
+
|
|
128
|
+
JSON.parse(response.body)
|
|
129
|
+
rescue JSON::ParserError
|
|
130
|
+
response.body
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def backoff_seconds(attempt)
|
|
134
|
+
[0.5 * (2**attempt), 8].min
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def rate_limit_delay_seconds(response, attempt)
|
|
138
|
+
reset = response["X-RateLimit-Reset"].to_f
|
|
139
|
+
return backoff_seconds(attempt) unless reset.positive?
|
|
140
|
+
|
|
141
|
+
[[reset - Time.now.to_f, 0].max, 30].min
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Riveter
|
|
4
|
+
# Base class for every error raised by this SDK.
|
|
5
|
+
class Error < StandardError; end
|
|
6
|
+
|
|
7
|
+
# Network-level failure: the request never produced an HTTP response.
|
|
8
|
+
class APIConnectionError < Error; end
|
|
9
|
+
|
|
10
|
+
# A request or wait helper exceeded its time budget.
|
|
11
|
+
class APITimeoutError < APIConnectionError; end
|
|
12
|
+
|
|
13
|
+
# A non-2xx HTTP response, normalized across both error body dialects.
|
|
14
|
+
class APIError < Error
|
|
15
|
+
FALLBACK_TYPES = {
|
|
16
|
+
400 => "bad_request",
|
|
17
|
+
401 => "unauthorized",
|
|
18
|
+
403 => "forbidden",
|
|
19
|
+
404 => "not_found",
|
|
20
|
+
409 => "conflict",
|
|
21
|
+
422 => "validation",
|
|
22
|
+
429 => "rate_limited"
|
|
23
|
+
}.freeze
|
|
24
|
+
|
|
25
|
+
attr_reader :status, :type, :details
|
|
26
|
+
|
|
27
|
+
def initialize(status:, type:, message:, details: nil)
|
|
28
|
+
super(message)
|
|
29
|
+
@status = status
|
|
30
|
+
@type = type
|
|
31
|
+
@details = details
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Builds an APIError from either the clean { "error" => {...} } envelope
|
|
35
|
+
# or the legacy { "request_status" => "error" } shape (401s).
|
|
36
|
+
def self.from_response(status, body)
|
|
37
|
+
if body.is_a?(Hash)
|
|
38
|
+
error = body["error"]
|
|
39
|
+
if error.is_a?(Hash) && error["type"].is_a?(String)
|
|
40
|
+
return new(
|
|
41
|
+
status: status,
|
|
42
|
+
type: error["type"],
|
|
43
|
+
message: error["message"] || "HTTP #{status}",
|
|
44
|
+
details: error["details"]
|
|
45
|
+
)
|
|
46
|
+
end
|
|
47
|
+
if body["request_status"] == "error"
|
|
48
|
+
return new(
|
|
49
|
+
status: status,
|
|
50
|
+
type: body["error_type"] || "error",
|
|
51
|
+
message: body["message"] || "HTTP #{status}",
|
|
52
|
+
details: body["errors"]
|
|
53
|
+
)
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
message = body.is_a?(String) && !body.empty? ? body[0, 200] : "HTTP #{status}"
|
|
58
|
+
new(status: status, type: FALLBACK_TYPES[status] || "http_#{status}", message: message)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Riveter
|
|
4
|
+
# Read-only view over a response hash: declared fields become reader methods,
|
|
5
|
+
# unknown fields stay reachable via #raw / #[].
|
|
6
|
+
class Model
|
|
7
|
+
attr_reader :raw
|
|
8
|
+
|
|
9
|
+
def initialize(raw)
|
|
10
|
+
@raw = raw.is_a?(Hash) ? raw : {}
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def [](key)
|
|
14
|
+
@raw[key.to_s]
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def to_h
|
|
18
|
+
@raw
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def inspect
|
|
22
|
+
"#<#{self.class.name} #{@raw.inspect}>"
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def self.field(*names)
|
|
26
|
+
names.each do |name|
|
|
27
|
+
define_method(name) { @raw[name.to_s] }
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def self.model_field(name, model_class)
|
|
32
|
+
define_method(name) do
|
|
33
|
+
value = @raw[name.to_s]
|
|
34
|
+
value.nil? ? nil : model_class.new(value)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
class RunProgress < Model
|
|
40
|
+
field :percent_complete, :estimated_seconds_remaining, :elapsed_seconds,
|
|
41
|
+
:completed_cells, :total_cells_expected, :not_found_cells
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# The uniform run envelope returned by kickoffs and every /runs endpoint.
|
|
45
|
+
# Endpoint-specific kickoff extras (dataset_id, credits_charged, ...) are
|
|
46
|
+
# plain fields; #output is nil until the run finishes.
|
|
47
|
+
class Run < Model
|
|
48
|
+
field :id, :type, :status, :credits_used, :app_url, :result_url,
|
|
49
|
+
:started_at, :finished_at, :error, :enrichment_id, :enrichment_name,
|
|
50
|
+
:dataset_id, :extraction_id, :monitor_id, :webhook_url, :output,
|
|
51
|
+
:max_items, :enrichment_run_id, :source_dataset_id,
|
|
52
|
+
:configured_dataset_id, :credits_charged, :variables,
|
|
53
|
+
:validation_warning, :tier
|
|
54
|
+
model_field :progress, RunProgress
|
|
55
|
+
|
|
56
|
+
def finished?
|
|
57
|
+
%w[success stopped].include?(status)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
class RunListItem < Model
|
|
62
|
+
field :id, :type, :status, :enrichment_id, :enrichment_name, :row_count,
|
|
63
|
+
:credits_used, :error, :created_at, :started_at, :finished_at,
|
|
64
|
+
:app_url, :result_url
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
class Pagination < Model
|
|
68
|
+
field :page, :per_page, :total_count, :total_pages
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
class RunsSummary < Model
|
|
72
|
+
field :counts
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
class EnrichmentSummary < Model
|
|
76
|
+
field :id, :name, :status, :app_url, :columns
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
class Enrichment < Model
|
|
80
|
+
field :id, :name, :status, :app_url, :input, :output, :dataset_id
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
class Extraction < Model
|
|
84
|
+
field :id, :name, :status, :app_url, :starting_url, :goal_description,
|
|
85
|
+
:output_record_json_schema, :required_keys, :locked,
|
|
86
|
+
:validation_passing, :discovered_at, :run_credits_required,
|
|
87
|
+
:credits_charged
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
class Monitor < Model
|
|
91
|
+
field :id, :name, :enabled, :cadence, :minute, :hour, :day_of_week,
|
|
92
|
+
:day_of_month, :timezone, :webhook_url, :alert_rule, :output_format,
|
|
93
|
+
:next_run_at, :schedule_summary, :enrichment_id, :enrichment_name,
|
|
94
|
+
:created_at, :has_input
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Synchronous scrape result (legacy request_status format, per the spec).
|
|
98
|
+
class ScrapeResult < Model
|
|
99
|
+
field :request_status, :text, :url, :base_url_for_links, :status_code,
|
|
100
|
+
:possibly_blocked, :credit_used, :riveter_app_link
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
class Credit < Model
|
|
104
|
+
field :count, :max, :balance
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
class Account < Model
|
|
108
|
+
field :uuid, :name, :plan
|
|
109
|
+
model_field :credit, Credit
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
class ApiKeyInfo < Model
|
|
113
|
+
field :name, :last_used_at, :created_by
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Account info (legacy request_status format, per the spec).
|
|
117
|
+
class AccountInfo < Model
|
|
118
|
+
field :request_status, :message
|
|
119
|
+
model_field :account, Account
|
|
120
|
+
model_field :api_key_info, ApiKeyInfo
|
|
121
|
+
end
|
|
122
|
+
end
|
data/lib/riveter/page.rb
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Riveter
|
|
4
|
+
# One page of runs. #each iterates the page; #auto_paging_each walks every
|
|
5
|
+
# following page too.
|
|
6
|
+
class RunsPage
|
|
7
|
+
include Enumerable
|
|
8
|
+
|
|
9
|
+
attr_reader :runs, :pagination, :monitor_id
|
|
10
|
+
|
|
11
|
+
def initialize(data, fetch_page)
|
|
12
|
+
data = data.is_a?(Hash) ? data : {}
|
|
13
|
+
@runs = (data["runs"] || []).map { |item| RunListItem.new(item) }
|
|
14
|
+
@pagination = Pagination.new(data["pagination"] || {})
|
|
15
|
+
@monitor_id = data["monitor_id"]
|
|
16
|
+
@fetch_page = fetch_page
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def each(&block)
|
|
20
|
+
@runs.each(&block)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def next_page?
|
|
24
|
+
(pagination.page || 1) < (pagination.total_pages || 1)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def next_page
|
|
28
|
+
RunsPage.new(@fetch_page.call((pagination.page || 1) + 1), @fetch_page)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def auto_paging_each(&block)
|
|
32
|
+
return enum_for(:auto_paging_each) unless block
|
|
33
|
+
|
|
34
|
+
page = self
|
|
35
|
+
loop do
|
|
36
|
+
page.runs.each(&block)
|
|
37
|
+
break unless page.next_page?
|
|
38
|
+
|
|
39
|
+
page = page.next_page
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Riveter
|
|
4
|
+
module Resources
|
|
5
|
+
# The uniform lifecycle for every async operation — status, results, stop.
|
|
6
|
+
class Runs
|
|
7
|
+
def initialize(client)
|
|
8
|
+
@client = client
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
# Status and progress of a run. (operationId: getRun)
|
|
12
|
+
def get(run_id)
|
|
13
|
+
Run.new(@client.request(:get, "/runs/#{run_id}"))
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# The run plus its #output (nil until finished). `wait` long-polls up to
|
|
17
|
+
# 50 seconds server-side. (operationId: getRunResult)
|
|
18
|
+
def result(run_id, wait: nil)
|
|
19
|
+
timeout = wait ? [@client.timeout, wait + 10].max : nil
|
|
20
|
+
Run.new(@client.request(:get, "/runs/#{run_id}/result", query: { wait: wait }, timeout: timeout))
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Stop a run early. (operationId: stopRun)
|
|
24
|
+
def stop(run_id)
|
|
25
|
+
Run.new(@client.request(:post, "/runs/#{run_id}/stop"))
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# List the account's runs, newest first. (operationId: listRuns)
|
|
29
|
+
def list(**params)
|
|
30
|
+
fetch_page = ->(page) { @client.request(:get, "/runs", query: params.merge(page: page)) }
|
|
31
|
+
RunsPage.new(fetch_page.call(params[:page] || 1), fetch_page)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# All-time run counts by status. (operationId: runsSummary)
|
|
35
|
+
def summary
|
|
36
|
+
RunsSummary.new(@client.request(:get, "/runs/summary"))
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Polls #result with 50s long-polls until the run reaches success or
|
|
40
|
+
# stopped; raises APITimeoutError past the timeout (seconds).
|
|
41
|
+
def wait_for_result(run_id, timeout: 600, poll_wait: 50)
|
|
42
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + timeout
|
|
43
|
+
loop do
|
|
44
|
+
run = result(run_id, wait: poll_wait)
|
|
45
|
+
return run if run.finished?
|
|
46
|
+
|
|
47
|
+
if Process.clock_gettime(Process::CLOCK_MONOTONIC) >= deadline
|
|
48
|
+
raise APITimeoutError, "Run #{run_id} did not finish within #{timeout}s"
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Saved enrichment configurations.
|
|
55
|
+
class Enrichments
|
|
56
|
+
def initialize(client)
|
|
57
|
+
@client = client
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# List enrichments with their output column configuration. (operationId: listEnrichments)
|
|
61
|
+
def list
|
|
62
|
+
response = @client.request(:get, "/enrichments")
|
|
63
|
+
(response["enrichments"] || []).map { |item| EnrichmentSummary.new(item) }
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Create an enrichment (no run) from a completed dataset build. (operationId: createEnrichment)
|
|
67
|
+
def create(dataset_id:)
|
|
68
|
+
Enrichment.new(@client.request(:post, "/enrichments", body: { dataset_id: dataset_id }))
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# The enrichment's input columns and output column configuration. (operationId: getEnrichment)
|
|
72
|
+
def get(enrichment_id)
|
|
73
|
+
Enrichment.new(@client.request(:get, "/enrichments/#{enrichment_id}"))
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Add, update, rename, delete, or reorder output columns. (operationId: updateEnrichment)
|
|
77
|
+
def update(enrichment_id, **params)
|
|
78
|
+
@client.request(:patch, "/enrichments/#{enrichment_id}", body: params)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Build a dataset shaped for this enrichment. (operationId: buildDatasetForEnrichment)
|
|
82
|
+
def build_dataset(enrichment_id, **params)
|
|
83
|
+
Run.new(@client.request(:post, "/enrichments/#{enrichment_id}/datasets", body: params))
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Dataset builds — generate rows from prompts or structured specs.
|
|
88
|
+
class Datasets
|
|
89
|
+
def initialize(client)
|
|
90
|
+
@client = client
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Build a dataset from a prompt, a structured spec, or both. (operationId: buildDataset)
|
|
94
|
+
def build(**params)
|
|
95
|
+
Run.new(@client.request(:post, "/datasets", body: params))
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Generate new deduplicated rows for a completed dataset build.
|
|
99
|
+
# Named extend_dataset because Object#extend is taken. (operationId: extendDataset)
|
|
100
|
+
def extend_dataset(dataset_id, **params)
|
|
101
|
+
Run.new(@client.request(:post, "/datasets/#{dataset_id}/extend", body: params))
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Reusable, pre-configured dataset templates (cds_...).
|
|
106
|
+
class ConfiguredDatasets
|
|
107
|
+
def initialize(client)
|
|
108
|
+
@client = client
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Run a configured dataset template. (operationId: buildConfiguredDataset)
|
|
112
|
+
def build(configured_dataset_id, **params)
|
|
113
|
+
Run.new(@client.request(:post, "/configured_datasets/#{configured_dataset_id}/build", body: params))
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Reusable site scrape/extract recipes and their runs.
|
|
118
|
+
class Extractions
|
|
119
|
+
def initialize(client)
|
|
120
|
+
@client = client
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Create an extraction and start its agent discovery. (operationId: createExtraction)
|
|
124
|
+
def create(**params)
|
|
125
|
+
Extraction.new(@client.request(:post, "/extractions", body: params))
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# The extraction's status and definition. (operationId: getExtraction)
|
|
129
|
+
def get(extraction_id)
|
|
130
|
+
Extraction.new(@client.request(:get, "/extractions/#{extraction_id}"))
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Execute a ready extraction. (operationId: runExtraction)
|
|
134
|
+
def run(extraction_id, **params)
|
|
135
|
+
Run.new(@client.request(:post, "/extractions/#{extraction_id}/runs", body: params))
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Scheduled enrichment runs with webhooks.
|
|
140
|
+
class Monitors
|
|
141
|
+
def initialize(client)
|
|
142
|
+
@client = client
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Create a monitor that re-runs an enrichment on a schedule. (operationId: createMonitor)
|
|
146
|
+
def create(**params)
|
|
147
|
+
Monitor.new(@client.request(:post, "/monitors", body: params))
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# List the account's monitors, newest first. (operationId: listMonitors)
|
|
151
|
+
def list
|
|
152
|
+
response = @client.request(:get, "/monitors")
|
|
153
|
+
(response["monitors"] || []).map { |item| Monitor.new(item) }
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# The monitor's schedule, webhook, and next run time. (operationId: getMonitor)
|
|
157
|
+
def get(monitor_id)
|
|
158
|
+
Monitor.new(@client.request(:get, "/monitors/#{monitor_id}"))
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Pause, resume, or repoint a monitor. (operationId: updateMonitor)
|
|
162
|
+
def update(monitor_id, **params)
|
|
163
|
+
Monitor.new(@client.request(:patch, "/monitors/#{monitor_id}", body: params))
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# The monitor's run history, newest first. (operationId: listMonitorRuns)
|
|
167
|
+
def runs(monitor_id, **params)
|
|
168
|
+
fetch_page = lambda do |page|
|
|
169
|
+
@client.request(:get, "/monitors/#{monitor_id}/runs", query: params.merge(page: page))
|
|
170
|
+
end
|
|
171
|
+
RunsPage.new(fetch_page.call(params[:page] || 1), fetch_page)
|
|
172
|
+
end
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
end
|
data/lib/riveter.rb
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "riveter/version"
|
|
4
|
+
require "riveter/errors"
|
|
5
|
+
require "riveter/models"
|
|
6
|
+
require "riveter/page"
|
|
7
|
+
require "riveter/resources"
|
|
8
|
+
require "riveter/client"
|
|
9
|
+
|
|
10
|
+
# Official Ruby SDK for the Riveter API. https://docs.riveterhq.com
|
|
11
|
+
module Riveter
|
|
12
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: riveter-sdk
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Riveter
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies: []
|
|
12
|
+
description: Client for the Riveter API — enrich data, build datasets, scrape pages,
|
|
13
|
+
and run web searches. https://docs.riveterhq.com
|
|
14
|
+
email:
|
|
15
|
+
- support@riveterhq.com
|
|
16
|
+
executables: []
|
|
17
|
+
extensions: []
|
|
18
|
+
extra_rdoc_files: []
|
|
19
|
+
files:
|
|
20
|
+
- LICENSE
|
|
21
|
+
- README.md
|
|
22
|
+
- lib/riveter.rb
|
|
23
|
+
- lib/riveter/client.rb
|
|
24
|
+
- lib/riveter/errors.rb
|
|
25
|
+
- lib/riveter/models.rb
|
|
26
|
+
- lib/riveter/page.rb
|
|
27
|
+
- lib/riveter/resources.rb
|
|
28
|
+
- lib/riveter/version.rb
|
|
29
|
+
homepage: https://docs.riveterhq.com
|
|
30
|
+
licenses:
|
|
31
|
+
- MIT
|
|
32
|
+
metadata:
|
|
33
|
+
homepage_uri: https://docs.riveterhq.com
|
|
34
|
+
rubygems_mfa_required: 'true'
|
|
35
|
+
rdoc_options: []
|
|
36
|
+
require_paths:
|
|
37
|
+
- lib
|
|
38
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
39
|
+
requirements:
|
|
40
|
+
- - ">="
|
|
41
|
+
- !ruby/object:Gem::Version
|
|
42
|
+
version: '3.1'
|
|
43
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - ">="
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '0'
|
|
48
|
+
requirements: []
|
|
49
|
+
rubygems_version: 4.0.16
|
|
50
|
+
specification_version: 4
|
|
51
|
+
summary: Official Ruby SDK for the Riveter API
|
|
52
|
+
test_files: []
|