riveter-sdk 0.1.0 → 1.0.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 908379b467d0f1129dc3794df97bd927aeb3cdbdaaadd3146fe96c887f2835b9
4
- data.tar.gz: a23f012aaad173284d2759f1754de5bc5cf50d1225e437445774e3bc723245bc
3
+ metadata.gz: 867ffe82023d427f6584d46ad9d0e0e1799bbab22ac173a66eb542859d13fd7d
4
+ data.tar.gz: 6906da4985f0795a6074d4b3891f02cb27ccd02009165a13e9a550bb962449a6
5
5
  SHA512:
6
- metadata.gz: 38b1aac5504c3eac8c6256a0e559c02d4b5e526fb9a55de1d41214eeb0306be245c4dc991abbc17600fdabf11fc66dd96fb94383a23937575330c8c76bebcdb5
7
- data.tar.gz: a3e028f0a9e5db8842770b970c94a77b1525b0480b1564ca3e63dd30804e1a45eb2c9eabc0aee475b3496b35394ae599398d6965bc8afbd7fcaaa5c98f72818b
6
+ metadata.gz: 2663f0f05a85eb76a748775bae906427513d103d259ff9bab9cb7a8661ad8237bfbb65e3ce35c1cf94a39215749decd733640a8e739423a1358890f88c889bad
7
+ data.tar.gz: 45ef4e0f5b33e003ca0bc9916d0105f23d15a7fbd03280f5cd5ab84001884e86202673e48632f3e8100eee0e5fde2f8b58a8f17852aa7df0081f4c3c6375870d
data/README.md CHANGED
@@ -55,12 +55,35 @@ riveter.runs.list(status: "success").auto_paging_each do |run|
55
55
  end
56
56
  ```
57
57
 
58
+ ## Datasets
59
+
60
+ A dataset is a kept table: input rows you add, output columns an enrichment fills.
61
+
62
+ ```ruby
63
+ dataset = riveter.datasets.create(
64
+ name: "Wineries",
65
+ input_columns: ["Winery URL"],
66
+ rows: [{ "Winery URL" => "ridgewine.com" }, { "Winery URL" => "bonnydoonvineyard.com" }]
67
+ )
68
+ riveter.datasets.update(dataset.id, output: { "Owner" => { prompt: "The winery's current owner" } })
69
+ fill = riveter.enrich(dataset_id: dataset.id)
70
+ riveter.runs.wait_for_result(fill.id)
71
+ riveter.datasets.list_rows(dataset.id).auto_paging_each do |row|
72
+ puts [row.input["Winery URL"], row.cells["Owner"].value].join(": ")
73
+ end
74
+ csv = riveter.datasets.export(dataset.id)
75
+ ```
76
+
77
+ `datasets.create(prompt: ..., count: ...)` generates the rows instead (paid; pass
78
+ `estimate_cost: true` first). `datasets.import_rows(id, csv)` appends a CSV.
79
+
58
80
  ## Surface
59
81
 
60
- - `riveter.enrich(...)`, `riveter.quick_search(...)`, `riveter.scrape(...)`, `riveter.account`
82
+ - `riveter.enrich(...)`, `riveter.quick_search(...)`, `riveter.search_agent(...)`, `riveter.scrape(...)`, `riveter.account`
61
83
  - `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`
84
+ - `riveter.enrichments` — `list`, `create`, `get`, `update`, `delete`
85
+ - `riveter.datasets` — `list`, `create`, `get`, `update`, `delete`, `list_rows`, `add_rows`, `delete_rows`, `update_row`, `import_rows`, `build_rows`, `promote_column`, `export`
86
+ - `riveter.tools` — `list`
64
87
  - `riveter.configured_datasets` — `build`
65
88
  - `riveter.extractions` — `create`, `get`, `run`
66
89
  - `riveter.monitors` — `create`, `list`, `get`, `update`, `runs`
@@ -71,7 +94,8 @@ via `#raw` / `#[]`.
71
94
  ## Errors and retries
72
95
 
73
96
  API failures raise `Riveter::APIError` with `#status`, `#type` (`not_found`,
74
- `insufficient_credits`, ...), `#message`, and optional `#details`. Network failures raise
97
+ `insufficient_credits`, `feature_not_enabled`, ...; the known values are
98
+ `Riveter::APIError::TYPES`), `#message`, and optional `#details`. Network failures raise
75
99
  `Riveter::APIConnectionError` / `Riveter::APITimeoutError`. 429s are retried automatically
76
100
  using the `X-RateLimit-Reset` header; 5xx and network failures are retried for GETs.
77
101
  Configure with `max_retries:` and `timeout:`.
@@ -91,5 +115,5 @@ Riveter::Client.new(
91
115
 
92
116
  ```bash
93
117
  bundle install
94
- bundle exec rspec
118
+ bundle exec ruby -Ilib -e "require 'riveter'"
95
119
  ```
@@ -12,7 +12,7 @@ module Riveter
12
12
 
13
13
  attr_reader :base_url, :timeout, :max_retries,
14
14
  :runs, :enrichments, :datasets, :configured_datasets,
15
- :extractions, :monitors
15
+ :extractions, :monitors, :tools
16
16
 
17
17
  # api_key falls back to ENV["RIVETER_API_KEY"]; base_url falls back to
18
18
  # ENV["RIVETER_BASE_URL"], then production. timeout (seconds) must exceed
@@ -34,9 +34,16 @@ module Riveter
34
34
  @configured_datasets = Resources::ConfiguredDatasets.new(self)
35
35
  @extractions = Resources::Extractions.new(self)
36
36
  @monitors = Resources::Monitors.new(self)
37
+ @tools = Resources::Tools.new(self)
37
38
  end
38
39
 
39
40
  # Start an enrichment run on rows of input data. (operationId: enrich)
41
+ # Rows come from `input:` (inline) or `dataset_id:` (a Dataset); column config from
42
+ # `enrichment_id:`, `output:`, or `prompt:` + `attributes:`. On a
43
+ # Dataset, `columns:` / `rows:` / `positions:` (or `targets:` boxes) scope the fill and
44
+ # `states:` names the cell states to re-run (`force: true` runs every cell).
45
+ # `estimate_cost: true` prices the run instead of starting it; `max_credits`
46
+ # refuses a run whose estimated maximum exceeds it. Same on every paid method.
40
47
  def enrich(**params)
41
48
  Run.new(request(:post, "/enrich", body: params))
42
49
  end
@@ -46,6 +53,12 @@ module Riveter
46
53
  Run.new(request(:post, "/quick_search", body: params))
47
54
  end
48
55
 
56
+ # Ask one research question; long-polls up to `wait` seconds (default 50) and
57
+ # returns the answer in #output["result"]. (operationId: searchAgent)
58
+ def search_agent(**params)
59
+ Run.new(request(:post, "/search_agent", body: params))
60
+ end
61
+
49
62
  # Scrape a webpage and return its text synchronously. (operationId: scrape)
50
63
  def scrape(**params)
51
64
  ScrapeResult.new(request(:post, "/scrape", body: params))
@@ -56,18 +69,22 @@ module Riveter
56
69
  AccountInfo.new(request(:get, "/account"))
57
70
  end
58
71
 
59
- def request(method, path, query: nil, body: nil, timeout: nil)
72
+ # `body` is sent as JSON; `raw_body:` + `content_type:` send a body as-is (CSV imports).
73
+ # `response_format:` is :json (an empty or 204 body is nil), :text, or :bytes.
74
+ def request(method, path, query: nil, body: nil, raw_body: nil, content_type: nil, response_format: :json,
75
+ timeout: nil)
60
76
  uri = URI.parse(@base_url + path)
61
77
  if query
62
78
  compact = query.reject { |_key, value| value.nil? }
63
79
  uri.query = URI.encode_www_form(compact) unless compact.empty?
64
80
  end
65
81
 
82
+ encoded_body = encode_request_body(body, raw_body, content_type)
66
83
  retry_on_failure = method == :get
67
84
  attempt = 0
68
85
  loop do
69
86
  response = begin
70
- perform_http_request(method, uri, body, timeout || @timeout)
87
+ perform_http_request(method, uri, encoded_body, timeout || @timeout)
71
88
  rescue Net::OpenTimeout, Net::ReadTimeout => e
72
89
  if retry_on_failure && attempt < @max_retries
73
90
  sleep(backoff_seconds(attempt))
@@ -85,7 +102,7 @@ module Riveter
85
102
  end
86
103
 
87
104
  status = response.code.to_i
88
- return parse_json_body(response) if status.between?(200, 299)
105
+ return read_success_body(response, response_format) if status.between?(200, 299)
89
106
 
90
107
  if status == 429 && attempt < @max_retries
91
108
  sleep(rate_limit_delay_seconds(response, attempt))
@@ -104,24 +121,42 @@ module Riveter
104
121
 
105
122
  private
106
123
 
107
- def perform_http_request(method, uri, body, timeout)
124
+ REQUEST_CLASSES = {
125
+ get: Net::HTTP::Get, post: Net::HTTP::Post, patch: Net::HTTP::Patch, delete: Net::HTTP::Delete
126
+ }.freeze
127
+ private_constant :REQUEST_CLASSES
128
+
129
+ def perform_http_request(method, uri, encoded_body, timeout)
108
130
  http = Net::HTTP.new(uri.host, uri.port)
109
131
  http.use_ssl = uri.scheme == "https"
110
132
  http.open_timeout = timeout
111
133
  http.read_timeout = timeout
112
134
 
113
- request_class = { get: Net::HTTP::Get, post: Net::HTTP::Post, patch: Net::HTTP::Patch }.fetch(method)
114
- request = request_class.new(uri)
135
+ request = REQUEST_CLASSES.fetch(method).new(uri)
115
136
  request["Authorization"] = "Bearer #{@api_key}"
116
137
  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? })
138
+ if encoded_body
139
+ request["Content-Type"] = encoded_body[:content_type]
140
+ request.body = encoded_body[:content]
120
141
  end
121
142
 
122
143
  http.request(request)
123
144
  end
124
145
 
146
+ def encode_request_body(body, raw_body, content_type)
147
+ return { content: raw_body, content_type: content_type || "application/octet-stream" } if raw_body
148
+ return nil unless body
149
+
150
+ { content: JSON.generate(body.reject { |_key, value| value.nil? }), content_type: "application/json" }
151
+ end
152
+
153
+ def read_success_body(response, response_format)
154
+ return response.body.to_s.b if response_format == :bytes
155
+ return response.body.to_s if response_format == :text
156
+
157
+ parse_json_body(response)
158
+ end
159
+
125
160
  def parse_json_body(response)
126
161
  return nil if response.body.nil? || response.body.empty?
127
162
 
@@ -10,8 +10,19 @@ module Riveter
10
10
  # A request or wait helper exceeded its time budget.
11
11
  class APITimeoutError < APIConnectionError; end
12
12
 
13
- # A non-2xx HTTP response, normalized across both error body dialects.
13
+ # A non-2xx HTTP response, normalized across both error body dialects. #type is one of
14
+ # TYPES, or a type this SDK does not know yet.
14
15
  class APIError < Error
16
+ # The error types the API documents. `feature_not_enabled` (403) means the account has
17
+ # not migrated to Datasets: stop retrying and open https://app.riveterhq.com/v2/datasets.
18
+ # The 409 trio (`dataset_in_use`, `enrichment_in_use`, `row_in_use`) means a fill is
19
+ # running: wait or stop it.
20
+ TYPES = %w[
21
+ bad_request validation not_found forbidden unauthorized feature_not_enabled
22
+ duplicate_run_key insufficient_credits credit_cap_exceeded not_implemented
23
+ dataset_in_use enrichment_in_use row_in_use payload_too_large rate_limited
24
+ ].freeze
25
+
15
26
  FALLBACK_TYPES = {
16
27
  400 => "bad_request",
17
28
  401 => "unauthorized",
@@ -41,17 +41,32 @@ module Riveter
41
41
  :completed_cells, :total_cells_expected, :not_found_cells
42
42
  end
43
43
 
44
+ # Kickoff-time estimate. #maximum is the contract — a run never charges more.
45
+ class CreditEstimate < Model
46
+ field :minimum, :maximum, :charged_upfront
47
+ end
48
+
49
+ # The estimate next to what has been charged. #credits_remaining and
50
+ # #sufficient_credits are present on estimates and credit errors only.
51
+ class CreditDetail < Model
52
+ field :credits_used, :credits_remaining, :sufficient_credits
53
+ model_field :estimate, CreditEstimate
54
+ end
55
+
44
56
  # The uniform run envelope returned by kickoffs and every /runs endpoint.
45
57
  # Endpoint-specific kickoff extras (dataset_id, credits_charged, ...) are
46
- # plain fields; #output is nil until the run finishes.
58
+ # plain fields; #output is nil until the run finishes. An `estimate_cost: true`
59
+ # kickoff returns this shape with #estimate_cost set, no #id, and the priced
60
+ # #credit_detail. #legacy ({"notice", "successor", "docs"}) is set when the
61
+ # kickoff took a legacy mode.
47
62
  class Run < Model
48
- field :id, :type, :status, :credits_used, :app_url, :result_url,
49
- :started_at, :finished_at, :error, :enrichment_id, :enrichment_name,
63
+ field :id, :type, :status, :credits_used, :estimate_cost, :app_url, :result_url,
64
+ :started_at, :finished_at, :error, :stopped_reason, :enrichment_id, :enrichment_name,
50
65
  :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
66
+ :max_items, :configured_dataset_id, :credits_charged, :variables,
67
+ :validation_warning, :tier, :legacy
54
68
  model_field :progress, RunProgress
69
+ model_field :credit_detail, CreditDetail
55
70
 
56
71
  def finished?
57
72
  %w[success stopped].include?(status)
@@ -60,7 +75,7 @@ module Riveter
60
75
 
61
76
  class RunListItem < Model
62
77
  field :id, :type, :status, :enrichment_id, :enrichment_name, :row_count,
63
- :credits_used, :error, :created_at, :started_at, :finished_at,
78
+ :credits_used, :error, :stopped_reason, :created_at, :started_at, :finished_at,
64
79
  :app_url, :result_url
65
80
  end
66
81
 
@@ -72,32 +87,143 @@ module Riveter
72
87
  field :counts
73
88
  end
74
89
 
90
+ # Compact list-endpoint shape. Column configuration is on Enrichments#get. #status,
91
+ # #app_url, and #dataset_id (the dataset copy to fill instead) are set on legacy
92
+ # enrichments only.
75
93
  class EnrichmentSummary < Model
76
- field :id, :name, :status, :app_url, :columns
94
+ field :id, :name, :status, :app_url, :dataset_id, :column_count, :input_columns,
95
+ :output_columns, :column_names_truncated, :created_at, :updated_at
77
96
  end
78
97
 
98
+ # An enrichment: #prompt, #input_columns, #output as an array of column hashes ("name" /
99
+ # "prompt" / "output_schema" / "tool"), #tools, the budget fields, and #version.
100
+ # #dataset_id / #app_url are set on Enrichments#create. A legacy enrichment's id answers
101
+ # its older shape (the legacy API docs, kept in #raw) with #legacy set.
79
102
  class Enrichment < Model
80
- field :id, :name, :status, :app_url, :input, :output, :dataset_id
103
+ field :id, :name, :prompt, :input_columns, :output, :tools, :budget_credits_per_row,
104
+ :budget_locked, :version, :created_at, :updated_at, :app_url, :dataset_id, :legacy
105
+ end
106
+
107
+ # One cell. #state is pending / running / ok / not_found / failed / manual / stale;
108
+ # #reason and #error are set on failed cells only.
109
+ class DatasetCell < Model
110
+ field :value, :state, :reason, :error, :sources
111
+ end
112
+
113
+ # A dataset row: #input by input column name, #cells by output column name
114
+ # (DatasetCell each). #position is 1-based; #credits is the row's charge across its runs.
115
+ class DatasetRow < Model
116
+ field :id, :position, :input, :credits, :not_found_reason, :created_at, :updated_at
117
+
118
+ def cells
119
+ (@raw["cells"] || {}).transform_values { |cell| DatasetCell.new(cell) }
120
+ end
121
+ end
122
+
123
+ # A run on a dataset as the dataset lists it; #type is dataset_build or enrichment.
124
+ class DatasetRunSummary < Model
125
+ field :id, :type, :status, :created_at, :finished_at
126
+ end
127
+
128
+ # A dataset (ds_...). #run is set on Datasets#create / #build_rows when a build or fill
129
+ # started; the column configuration is on Enrichments#get(enrichment_id).
130
+ class Dataset < Model
131
+ field :id, :name, :input_columns, :index_column, :enrichment_id, :output_columns,
132
+ :row_count, :rows_expired, :import_in_progress, :estimate_cost, :created_at, :updated_at
133
+ model_field :run, Run
134
+ model_field :credit_detail, CreditDetail
135
+
136
+ def runs
137
+ (@raw["runs"] || []).map { |run| DatasetRunSummary.new(run) }
138
+ end
139
+ end
140
+
141
+ class DatasetListItem < Model
142
+ field :id, :name, :input_column_count, :output_column_count, :row_count, :rows_expired,
143
+ :created_at, :updated_at
144
+ model_field :newest_run, DatasetRunSummary
145
+ end
146
+
147
+ class DatasetRowsAdded < Model
148
+ field :skipped_duplicates, :row_count
149
+
150
+ def rows
151
+ (@raw["rows"] || []).map { |row| DatasetRow.new(row) }
152
+ end
153
+ end
154
+
155
+ class DatasetRowsDeleted < Model
156
+ field :deleted, :row_count
157
+ end
158
+
159
+ # #status is "done" when the rows landed in the call, "importing" when a job is appending
160
+ # them (Datasets#get reports #import_in_progress until it does).
161
+ class DatasetImportResult < Model
162
+ field :row_count
163
+
164
+ def status
165
+ import_hash["status"]
166
+ end
167
+
168
+ def rows
169
+ import_hash["rows"]
170
+ end
171
+
172
+ def rows_added
173
+ import_hash["rows_added"]
174
+ end
175
+
176
+ def skipped_duplicates
177
+ import_hash["skipped_duplicates"]
178
+ end
179
+
180
+ private
181
+
182
+ def import_hash
183
+ @raw["import"] || {}
184
+ end
185
+ end
186
+
187
+ class ToolCatalogEntry < Model
188
+ field :name, :credits_per_call, :price_note, :default, :requires_opt_in, :enabled,
189
+ :requires_api_key, :api_key_linked, :companions, :inputs, :input_groups, :returns
190
+ end
191
+
192
+ # Every tool with its price, whether this account may use it, and the budget defaults.
193
+ class ToolCatalog < Model
194
+ field :budget
195
+
196
+ def tools
197
+ (@raw["tools"] || []).map { |tool| ToolCatalogEntry.new(tool) }
198
+ end
81
199
  end
82
200
 
83
201
  class Extraction < Model
84
202
  field :id, :name, :status, :app_url, :starting_url, :goal_description,
85
203
  :output_record_json_schema, :required_keys, :locked,
86
204
  :validation_passing, :discovered_at, :run_credits_required,
87
- :credits_charged
205
+ :credits_charged, :estimate_cost
206
+ model_field :credit_detail, CreditDetail
88
207
  end
89
208
 
209
+ # A monitor fills #dataset_id's #columns (every output column when nil) again on a
210
+ # schedule. #enrichment_name and #has_input belong to a monitor on a legacy enrichment;
211
+ # #legacy is true on such a list item and the notice hash on Monitors#get / #update. An
212
+ # `estimate_cost: true` create returns this shape with #estimate_cost set, no #id, and
213
+ # the priced #credit_detail for one tick.
90
214
  class Monitor < Model
91
215
  field :id, :name, :enabled, :cadence, :minute, :hour, :day_of_week,
92
216
  :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
217
+ :next_run_at, :schedule_summary, :dataset_id, :dataset_name, :columns,
218
+ :enrichment_id, :enrichment_name, :created_at, :has_input, :legacy, :estimate_cost
219
+ model_field :credit_detail, CreditDetail
95
220
  end
96
221
 
97
222
  # Synchronous scrape result (legacy request_status format, per the spec).
98
223
  class ScrapeResult < Model
99
224
  field :request_status, :text, :url, :base_url_for_links, :status_code,
100
- :possibly_blocked, :credit_used, :riveter_app_link
225
+ :possibly_blocked, :credit_used, :riveter_app_link, :estimate_cost
226
+ model_field :credit_detail, CreditDetail
101
227
  end
102
228
 
103
229
  class Credit < Model
data/lib/riveter/page.rb CHANGED
@@ -1,6 +1,87 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Riveter
4
+ # One page of enrichments. #each iterates the page; #auto_paging_each walks
5
+ # every following page too.
6
+ class EnrichmentsPage
7
+ include Enumerable
8
+
9
+ attr_reader :enrichments, :pagination
10
+
11
+ def initialize(data, fetch_page)
12
+ data = data.is_a?(Hash) ? data : {}
13
+ @enrichments = (data["enrichments"] || []).map { |item| EnrichmentSummary.new(item) }
14
+ @pagination = Pagination.new(data["pagination"] || {})
15
+ @fetch_page = fetch_page
16
+ end
17
+
18
+ def each(&block)
19
+ @enrichments.each(&block)
20
+ end
21
+
22
+ def next_page?
23
+ (pagination.page || 1) < (pagination.total_pages || 1)
24
+ end
25
+
26
+ def next_page
27
+ EnrichmentsPage.new(@fetch_page.call((pagination.page || 1) + 1), @fetch_page)
28
+ end
29
+
30
+ def auto_paging_each(&block)
31
+ return enum_for(:auto_paging_each) unless block
32
+
33
+ page = self
34
+ loop do
35
+ page.enrichments.each(&block)
36
+ break unless page.next_page?
37
+
38
+ page = page.next_page
39
+ end
40
+ end
41
+ end
42
+
43
+ # One page of dataset rows (cursor-paginated). #each iterates the page;
44
+ # #auto_paging_each walks every following page too.
45
+ class DatasetRowsPage
46
+ include Enumerable
47
+
48
+ attr_reader :rows, :next_cursor, :row_count
49
+
50
+ def initialize(data, fetch_page)
51
+ data = data.is_a?(Hash) ? data : {}
52
+ @rows = (data["rows"] || []).map { |item| DatasetRow.new(item) }
53
+ @next_cursor = data["next_cursor"]
54
+ @row_count = data["row_count"]
55
+ @fetch_page = fetch_page
56
+ end
57
+
58
+ def each(&block)
59
+ @rows.each(&block)
60
+ end
61
+
62
+ def next_page?
63
+ !next_cursor.nil?
64
+ end
65
+
66
+ def next_page
67
+ raise Error, "No next page" unless next_page?
68
+
69
+ DatasetRowsPage.new(@fetch_page.call(next_cursor), @fetch_page)
70
+ end
71
+
72
+ def auto_paging_each(&block)
73
+ return enum_for(:auto_paging_each) unless block
74
+
75
+ page = self
76
+ loop do
77
+ page.rows.each(&block)
78
+ break unless page.next_page?
79
+
80
+ page = page.next_page
81
+ end
82
+ end
83
+ end
84
+
4
85
  # One page of runs. #each iterates the page; #auto_paging_each walks every
5
86
  # following page too.
6
87
  class RunsPage
@@ -25,7 +25,8 @@ module Riveter
25
25
  Run.new(@client.request(:post, "/runs/#{run_id}/stop"))
26
26
  end
27
27
 
28
- # List the account's runs, newest first. (operationId: listRuns)
28
+ # List the account's runs, newest first. Params: type:, status:, enrichment_id:,
29
+ # dataset_id:, monitor_id:, created_after:, created_before:, page:, per_page:. (operationId: listRuns)
29
30
  def list(**params)
30
31
  fetch_page = ->(page) { @client.request(:get, "/runs", query: params.merge(page: page)) }
31
32
  RunsPage.new(fetch_page.call(params[:page] || 1), fetch_page)
@@ -57,15 +58,20 @@ module Riveter
57
58
  @client = client
58
59
  end
59
60
 
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) }
61
+ # Paginated compact list of enrichments: names, status, column names and
62
+ # counts. Params: status:, name:, page:, per_page:. (operationId: listEnrichments)
63
+ def list(**params)
64
+ fetch_page = ->(page) { @client.request(:get, "/enrichments", query: params.merge(page: page)) }
65
+ EnrichmentsPage.new(fetch_page.call(params[:page] || 1), fetch_page)
64
66
  end
65
67
 
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 }))
68
+ # Save an enrichment recipe (no run): `prompt:` + `output:` (column names, or hashes
69
+ # with name / prompt / output_schema / tool), optionally `name:`, `input_columns:`,
70
+ # `budget:`, `budget_locked:`, `only_tools:`, `additional_tools:`; an empty Dataset
71
+ # bound to it comes back as #dataset_id. `dataset_id:` alone takes a Dataset's own
72
+ # enrichment. (operationId: createEnrichment)
73
+ def create(**params)
74
+ Enrichment.new(@client.request(:post, "/enrichments", body: params))
69
75
  end
70
76
 
71
77
  # The enrichment's input columns and output column configuration. (operationId: getEnrichment)
@@ -73,32 +79,129 @@ module Riveter
73
79
  Enrichment.new(@client.request(:get, "/enrichments/#{enrichment_id}"))
74
80
  end
75
81
 
76
- # Add, update, rename, delete, or reorder output columns. (operationId: updateEnrichment)
82
+ # Edit the recipe: `name:`, `prompt:`, `input_columns:`, `budget:`, `budget_locked:`,
83
+ # the tool lists, and `output:` column operations keyed by column name (add with
84
+ # prompt / output_schema / tool, rename, delete). (operationId: updateEnrichment)
77
85
  def update(enrichment_id, **params)
78
- @client.request(:patch, "/enrichments/#{enrichment_id}", body: params)
86
+ Enrichment.new(@client.request(:patch, "/enrichments/#{enrichment_id}", body: params))
79
87
  end
80
88
 
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))
89
+ # Delete a saved recipe; datasets that used it keep their cells. Refused (409) while a
90
+ # fill runs on it. (operationId: deleteEnrichment)
91
+ def delete(enrichment_id)
92
+ @client.request(:delete, "/enrichments/#{enrichment_id}")
93
+ nil
84
94
  end
85
95
  end
86
96
 
87
- # Dataset builds — generate rows from prompts or structured specs.
97
+ # Datasets (ds_...): kept tables of input rows and enriched cells. Fill the cells with
98
+ # Client#enrich(dataset_id:).
88
99
  class Datasets
89
100
  def initialize(client)
90
101
  @client = client
91
102
  end
92
103
 
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))
104
+ # The account's datasets, newest first (up to 200). (operationId: listDatasets)
105
+ def list
106
+ response = @client.request(:get, "/datasets")
107
+ (response["datasets"] || []).map { |item| DatasetListItem.new(item) }
108
+ end
109
+
110
+ # Create a dataset: rows sent inline (`rows:`, ≤ 100 per call), generated from
111
+ # `prompt:` + `count:` (a paid list build, #run on the dataset; `estimate_cost: true`
112
+ # prices it), or empty (`name:` / `input_columns:` alone). `enrichment_id:` binds a
113
+ # saved recipe; `auto_fill: true` starts the fill once the rows land. (operationId: buildDataset)
114
+ def create(**params)
115
+ Dataset.new(@client.request(:post, "/datasets", body: params))
116
+ end
117
+
118
+ # The dataset's columns, row count, and runs. (operationId: getDataset)
119
+ def get(dataset_id)
120
+ Dataset.new(@client.request(:get, "/datasets/#{dataset_id}"))
121
+ end
122
+
123
+ # Rename (`name:`), move the index column (`index_column:`), add / rename / delete
124
+ # input and output columns (`input_columns:` / `output:` operations keyed by column
125
+ # name), and set the bound enrichment's `budget:` / `budget_locked:`, in one
126
+ # transaction. (operationId: updateDataset)
127
+ def update(dataset_id, **params)
128
+ Dataset.new(@client.request(:patch, "/datasets/#{dataset_id}", body: params))
129
+ end
130
+
131
+ # Delete the dataset and its rows. Refused (409) while a run is active on it. (operationId: deleteDataset)
132
+ def delete(dataset_id)
133
+ @client.request(:delete, "/datasets/#{dataset_id}")
134
+ nil
135
+ end
136
+
137
+ # A page of rows in position order (`limit:`, `cursor:`, `updated_since:`);
138
+ # #auto_paging_each walks every following page. (operationId: listDatasetRows)
139
+ def list_rows(dataset_id, **params)
140
+ fetch_page = lambda do |cursor|
141
+ @client.request(:get, "/datasets/#{dataset_id}/rows", query: params.merge(cursor: cursor))
142
+ end
143
+ DatasetRowsPage.new(fetch_page.call(params[:cursor]), fetch_page)
144
+ end
145
+
146
+ # Append rows (≤ 100 per call) keyed by input column name; duplicates on the index
147
+ # column are skipped. Free. (operationId: addDatasetRows)
148
+ def add_rows(dataset_id, rows)
149
+ DatasetRowsAdded.new(@client.request(:post, "/datasets/#{dataset_id}/rows", body: { rows: rows }))
96
150
  end
97
151
 
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))
152
+ # Remove up to 100 rows by `ids:` (row_...) and / or 1-based `positions:`. (operationId: deleteDatasetRows)
153
+ def delete_rows(dataset_id, ids: nil, positions: nil)
154
+ query = { ids: ids&.join(","), positions: positions&.join(",") }
155
+ DatasetRowsDeleted.new(@client.request(:delete, "/datasets/#{dataset_id}/rows", query: query))
156
+ end
157
+
158
+ # Set a row's `input:` values and / or `cells:` by hand (nil clears one; cells land as
159
+ # manual). `row_id` is a row id (row_...) or a 1-based position. (operationId: updateDatasetRow)
160
+ def update_row(dataset_id, row_id, **params)
161
+ DatasetRow.new(@client.request(:patch, "/datasets/#{dataset_id}/rows/#{row_id}", body: params))
162
+ end
163
+
164
+ # Append a CSV whose headers are input and / or output column names (output values
165
+ # land as manual cells). Small files land in the call (#status "done"); big ones are
166
+ # appended by a job ("importing"). (operationId: importDatasetRows)
167
+ def import_rows(dataset_id, csv)
168
+ DatasetImportResult.new(
169
+ @client.request(:post, "/datasets/#{dataset_id}/rows/import", raw_body: csv, content_type: "text/csv")
170
+ )
171
+ end
172
+
173
+ # Generate `count:` more rows from a prompt (paid; `estimate_cost: true` prices it).
174
+ # The build run is #run on the dataset. (operationId: buildDatasetRows)
175
+ def build_rows(dataset_id, **params)
176
+ Dataset.new(@client.request(:post, "/datasets/#{dataset_id}/rows/build", body: params))
177
+ end
178
+
179
+ # Turn an output column into an input column, every row's value moving with it. (operationId: promoteDatasetColumn)
180
+ def promote_column(dataset_id, column)
181
+ Dataset.new(@client.request(:post, "/datasets/#{dataset_id}/columns/promote", body: { column: column }))
182
+ end
183
+
184
+ # The grid as CSV (default) or JSON Lines text, or an Excel workbook
185
+ # (`output_format: "xlsx"`, a binary String). (operationId: exportDataset)
186
+ def export(dataset_id, output_format: "csv")
187
+ @client.request(
188
+ :get, "/datasets/#{dataset_id}/export",
189
+ query: { output_format: output_format },
190
+ response_format: output_format == "xlsx" ? :bytes : :text
191
+ )
192
+ end
193
+ end
194
+
195
+ # The tool catalog the row agent draws from.
196
+ class Tools
197
+ def initialize(client)
198
+ @client = client
199
+ end
200
+
201
+ # Every tool with its price, whether this account may use it, its inputs, and the
202
+ # budget defaults. (operationId: listTools)
203
+ def list
204
+ ToolCatalog.new(@client.request(:get, "/tools"))
102
205
  end
103
206
  end
104
207
 
@@ -136,13 +239,18 @@ module Riveter
136
239
  end
137
240
  end
138
241
 
139
- # Scheduled enrichment runs with webhooks.
242
+ # Monitors: a dataset's output columns filled again on a schedule, with diffs and webhooks.
140
243
  class Monitors
141
244
  def initialize(client)
142
245
  @client = client
143
246
  end
144
247
 
145
- # Create a monitor that re-runs an enrichment on a schedule. (operationId: createMonitor)
248
+ # Create a monitor that fills a dataset again on a schedule: `dataset_id:` (or
249
+ # `enrichment_id:` naming the dataset's enrichment), the cadence fields, optionally
250
+ # `columns:` (output column names each tick fills; every one when omitted),
251
+ # `alert_rule:` (each_run / on_diff / disabled), `output_format:` (current_only /
252
+ # include_diffs). Creation is free; `estimate_cost: true` prices ONE tick and
253
+ # `max_credits` refuses the monitor when that estimate is higher. (operationId: createMonitor)
146
254
  def create(**params)
147
255
  Monitor.new(@client.request(:post, "/monitors", body: params))
148
256
  end
@@ -158,7 +266,8 @@ module Riveter
158
266
  Monitor.new(@client.request(:get, "/monitors/#{monitor_id}"))
159
267
  end
160
268
 
161
- # Pause, resume, or repoint a monitor. (operationId: updateMonitor)
269
+ # Pause, resume, repoint the webhook, or change the `columns:` a tick fills
270
+ # (`columns: nil` means every output column again). (operationId: updateMonitor)
162
271
  def update(monitor_id, **params)
163
272
  Monitor.new(@client.request(:patch, "/monitors/#{monitor_id}", body: params))
164
273
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Riveter
4
- VERSION = "0.1.0"
4
+ VERSION = "1.0.1"
5
5
  end
metadata CHANGED
@@ -1,13 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: riveter-sdk
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 1.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Riveter
8
+ autorequire:
8
9
  bindir: bin
9
10
  cert_chain: []
10
- date: 1980-01-02 00:00:00.000000000 Z
11
+ date: 2026-10-27 00:00:00.000000000 Z
11
12
  dependencies: []
12
13
  description: Client for the Riveter API — enrich data, build datasets, scrape pages,
13
14
  and run web searches. https://docs.riveterhq.com
@@ -32,6 +33,7 @@ licenses:
32
33
  metadata:
33
34
  homepage_uri: https://docs.riveterhq.com
34
35
  rubygems_mfa_required: 'true'
36
+ post_install_message:
35
37
  rdoc_options: []
36
38
  require_paths:
37
39
  - lib
@@ -46,7 +48,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
46
48
  - !ruby/object:Gem::Version
47
49
  version: '0'
48
50
  requirements: []
49
- rubygems_version: 4.0.16
51
+ rubygems_version: 3.5.22
52
+ signing_key:
50
53
  specification_version: 4
51
54
  summary: Official Ruby SDK for the Riveter API
52
55
  test_files: []