Class: VisionAPI::Client
- Inherits:
-
Object
- Object
- VisionAPI::Client
- Defined in:
- lib/vision_api/client.rb
Overview
The Vision API client.
One object, one method per endpoint, plus the two things every integration ends up
writing by hand: retry that honors Retry-After, and polling that knows when a task is
done. Docs: https://docs.visionapi.io
vision = VisionAPI::Client.new # reads ENV["VISION_API_KEY"]
res = vision.analyze(file: "invoice.pdf", preset: "invoice")
res["result"]["invoice_id"]["value"] # => "A-10422"
Responses are plain hashes with the wire's snake_case keys, because the response
shape is the public contract and renaming it here would make
https://docs.visionapi.io stop matching what you see in your editor.
Constant Summary collapse
- RETRYABLE_STATUS =
[429, 500, 502, 503].freeze
Instance Attribute Summary collapse
-
#base_url ⇒ Object
readonly
Returns the value of attribute base_url.
-
#max_retries ⇒ Object
readonly
Returns the value of attribute max_retries.
-
#timeout ⇒ Object
readonly
Returns the value of attribute timeout.
Instance Method Summary collapse
-
#analyze(file: nil, file_url: nil, file_base64: nil, idempotency_key: nil, timeout: nil, **params) ⇒ Hash
Extracts structured data from one image or PDF and waits for the answer.
-
#analyze_and_wait(poll_interval: 2, max_wait: 600, on_poll: nil, **params) ⇒ Object
#analyze_async followed by #wait_for_task — the shape most batch jobs want.
-
#analyze_async(file: nil, file_url: nil, file_base64: nil, webhook_url: nil, idempotency_key: nil, timeout: nil, **params) ⇒ Hash
Submits an extraction to the queue and returns as soon as it is accepted.
-
#ask(questions:, file: nil, file_url: nil, file_base64: nil, pages: nil, language_hint: nil, detail: nil, idempotency_key: nil, timeout: nil) ⇒ Object
Asks up to 5 questions about one file.
-
#ask_async(questions:, file: nil, file_url: nil, file_base64: nil, pages: nil, language_hint: nil, detail: nil, webhook_url: nil, idempotency_key: nil, timeout: nil) ⇒ Object
Queues an #ask instead of waiting for it.
-
#create_schema(name, preset: nil, schema: nil, timeout: nil) ⇒ Object
Saves a preset + custom-field combination under a name, for later use as analyze(schema_name: …).
-
#credits(timeout: nil) ⇒ Object
Balance and the per-bucket breakdown.
- #delete_schema(name, timeout: nil) ⇒ Object
-
#detect(file: nil, file_url: nil, file_base64: nil, detail: nil, idempotency_key: nil, timeout: nil) ⇒ Object
Identifies what a file is without paying to extract it.
-
#each_request(limit: 100, &block) ⇒ Enumerator
Enumerates the whole history, one record at a time, paging as it goes.
-
#get_task(task_id, timeout: nil) ⇒ Object
Status and, once finished, the result of an async task.
-
#initialize(api_key: nil, base_url: nil, timeout: 120, max_retries: 3, auto_idempotency: true, headers: {}) ⇒ Client
constructor
A new instance of Client.
-
#preset(name, timeout: nil) ⇒ Object
One preset with its full field definitions.
-
#presets(timeout: nil) ⇒ Object
The preset catalog.
-
#requests(limit: nil, cursor: nil, timeout: nil) ⇒ Object
One page of usage history, newest first.
- #schema(name, timeout: nil) ⇒ Object
-
#schemas(timeout: nil) ⇒ Object
Every schema saved on this account.
-
#update_schema(name, preset: nil, schema: nil, timeout: nil) ⇒ Object
Replaces a saved schema in place.
-
#wait_for_task(task_id, poll_interval: 2, max_wait: 600, raise_on_failure: true, on_poll: nil) ⇒ Object
Polls until the task leaves +queued+/+processing+, then returns it.
Constructor Details
#initialize(api_key: nil, base_url: nil, timeout: 120, max_retries: 3, auto_idempotency: true, headers: {}) ⇒ Client
Returns a new instance of Client.
45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 |
# File 'lib/vision_api/client.rb', line 45 def initialize(api_key: nil, base_url: nil, timeout: 120, max_retries: 3, auto_idempotency: true, headers: {}) @api_key = api_key || ENV.fetch("VISION_API_KEY", nil) if @api_key.nil? || @api_key.empty? raise UsageError, "No API key. Pass VisionAPI::Client.new(api_key: …) or set " \ "VISION_API_KEY. Create one at https://app.visionapi.io/dashboard/keys" end @base_url = (base_url || ENV["VISION_API_URL"] || DEFAULT_BASE_URL).sub(%r{/+\z}, "") @timeout = timeout @max_retries = max_retries @auto_idempotency = auto_idempotency @headers = headers @user_agent = "visionapi-ruby/#{VisionAPI::VERSION} ruby/#{RUBY_VERSION}" end |
Instance Attribute Details
#base_url ⇒ Object (readonly)
Returns the value of attribute base_url.
30 31 32 |
# File 'lib/vision_api/client.rb', line 30 def base_url @base_url end |
#max_retries ⇒ Object (readonly)
Returns the value of attribute max_retries.
30 31 32 |
# File 'lib/vision_api/client.rb', line 30 def max_retries @max_retries end |
#timeout ⇒ Object (readonly)
Returns the value of attribute timeout.
30 31 32 |
# File 'lib/vision_api/client.rb', line 30 def timeout @timeout end |
Instance Method Details
#analyze(file: nil, file_url: nil, file_base64: nil, idempotency_key: nil, timeout: nil, **params) ⇒ Hash
Extracts structured data from one image or PDF and waits for the answer.
Costs 1 credit per image, or 1 per selected PDF page. Failures cost nothing — the reservation is released in full on any non-2xx, so there is no compensating logic to write. Anything that might run past 60 s belongs on #analyze_async.
84 85 86 87 88 |
# File 'lib/vision_api/client.rb', line 84 def analyze(file: nil, file_url: nil, file_base64: nil, idempotency_key: nil, timeout: nil, **params) submit("/v1/analyze", file, file_url, file_base64, analyze_fields(**params), idempotency_key, timeout) end |
#analyze_and_wait(poll_interval: 2, max_wait: 600, on_poll: nil, **params) ⇒ Object
#analyze_async followed by #wait_for_task — the shape most batch jobs want.
107 108 109 110 |
# File 'lib/vision_api/client.rb', line 107 def analyze_and_wait(poll_interval: 2, max_wait: 600, on_poll: nil, **params) ref = analyze_async(**params) wait_for_task(ref["task_id"], poll_interval: poll_interval, max_wait: max_wait, on_poll: on_poll) end |
#analyze_async(file: nil, file_url: nil, file_base64: nil, webhook_url: nil, idempotency_key: nil, timeout: nil, **params) ⇒ Hash
Submits an extraction to the queue and returns as soon as it is accepted.
Use it for long PDFs, detail: "high", or any batch you do not want to hold a
connection open for. Poll with #wait_for_task, or pass webhook_url — an HTTPS
endpoint that does not resolve to a private address — and let the result come to you.
97 98 99 100 101 |
# File 'lib/vision_api/client.rb', line 97 def analyze_async(file: nil, file_url: nil, file_base64: nil, webhook_url: nil, idempotency_key: nil, timeout: nil, **params) fields = analyze_fields(**params).merge("async" => true, "webhook_url" => webhook_url) submit("/v1/analyze", file, file_url, file_base64, fields, idempotency_key, timeout) end |
#ask(questions:, file: nil, file_url: nil, file_base64: nil, pages: nil, language_hint: nil, detail: nil, idempotency_key: nil, timeout: nil) ⇒ Object
Asks up to 5 questions about one file.
Priced exactly like an extraction — per image or per selected page — and the
questions themselves are free. Branch on answer
(+yes+ / no / uncertain / n/a) rather than parsing the prose.
117 118 119 120 121 |
# File 'lib/vision_api/client.rb', line 117 def ask(questions:, file: nil, file_url: nil, file_base64: nil, pages: nil, language_hint: nil, detail: nil, idempotency_key: nil, timeout: nil) fields = ask_fields(questions, pages, language_hint, detail) submit("/v1/ask", file, file_url, file_base64, fields, idempotency_key, timeout) end |
#ask_async(questions:, file: nil, file_url: nil, file_base64: nil, pages: nil, language_hint: nil, detail: nil, webhook_url: nil, idempotency_key: nil, timeout: nil) ⇒ Object
Queues an #ask instead of waiting for it.
124 125 126 127 128 129 130 |
# File 'lib/vision_api/client.rb', line 124 def ask_async(questions:, file: nil, file_url: nil, file_base64: nil, pages: nil, language_hint: nil, detail: nil, webhook_url: nil, idempotency_key: nil, timeout: nil) fields = ask_fields(questions, pages, language_hint, detail) .merge("async" => true, "webhook_url" => webhook_url) submit("/v1/ask", file, file_url, file_base64, fields, idempotency_key, timeout) end |
#create_schema(name, preset: nil, schema: nil, timeout: nil) ⇒ Object
Saves a preset + custom-field combination under a name, for later use as analyze(schema_name: …). The definition is compiled before it is stored, so an invalid schema fails here rather than on the first extraction that uses it.
241 242 243 244 245 246 |
# File 'lib/vision_api/client.rb', line 241 def create_schema(name, preset: nil, schema: nil, timeout: nil) body = { "name" => name } body["preset"] = preset if preset body["schema"] = schema if schema request(:post, "/v1/schemas", json: body, timeout: timeout) end |
#credits(timeout: nil) ⇒ Object
Balance and the per-bucket breakdown. Buckets are spent in order: subscription → rollover → pack → welcome.
189 190 191 |
# File 'lib/vision_api/client.rb', line 189 def credits(timeout: nil) request(:get, "/v1/credits", timeout: timeout) end |
#delete_schema(name, timeout: nil) ⇒ Object
256 257 258 259 |
# File 'lib/vision_api/client.rb', line 256 def delete_schema(name, timeout: nil) request(:delete, "/v1/schemas/#{escape_segment(name)}", timeout: timeout) nil end |
#detect(file: nil, file_url: nil, file_base64: nil, detail: nil, idempotency_key: nil, timeout: nil) ⇒ Object
Identifies what a file is without paying to extract it.
Metered at 1 credit per 10 calls whatever the page count, because detection only ever reads page 1 — so nine calls out of ten report "credits_used" => 0. "recommended" is exactly what preset: "auto" would have run, so you can probe first and trust the answer.
Reach for it when the type is the decision (routing a mixed inbox, refusing to spend on a 40-page PDF sight unseen); reach for preset: "auto" when you want the data and do not care which preset produced it.
142 143 144 145 146 |
# File 'lib/vision_api/client.rb', line 142 def detect(file: nil, file_url: nil, file_base64: nil, detail: nil, idempotency_key: nil, timeout: nil) submit("/v1/detect", file, file_url, file_base64, { "detail" => detail }, idempotency_key, timeout) end |
#each_request(limit: 100, &block) ⇒ Enumerator
Enumerates the whole history, one record at a time, paging as it goes.
202 203 204 205 206 207 208 209 210 211 212 |
# File 'lib/vision_api/client.rb', line 202 def each_request(limit: 100, &block) return enum_for(:each_request, limit: limit) unless block_given? cursor = nil loop do page = requests(limit: limit, cursor: cursor) page["data"].each(&block) cursor = page["next_cursor"] break if cursor.nil? || cursor.empty? end end |
#get_task(task_id, timeout: nil) ⇒ Object
Status and, once finished, the result of an async task.
Results stay retrievable for 7 days; after that this raises ResultExpiredError (the metadata survives, the payload does not).
154 155 156 |
# File 'lib/vision_api/client.rb', line 154 def get_task(task_id, timeout: nil) request(:get, "/v1/tasks/#{escape_segment(task_id)}", timeout: timeout) end |
#preset(name, timeout: nil) ⇒ Object
One preset with its full field definitions. Read this before mapping field names.
223 224 225 |
# File 'lib/vision_api/client.rb', line 223 def preset(name, timeout: nil) request(:get, "/v1/presets/#{escape_segment(name)}", auth: false, timeout: timeout) end |
#presets(timeout: nil) ⇒ Object
The preset catalog. No API key required.
Pick a preset from here rather than from memory — presets are versioned, and the catalog is the source of truth for both the names and the fields.
218 219 220 |
# File 'lib/vision_api/client.rb', line 218 def presets(timeout: nil) request(:get, "/v1/presets", auth: false, timeout: timeout)["data"] end |
#requests(limit: nil, cursor: nil, timeout: nil) ⇒ Object
One page of usage history, newest first. Metadata only — the file and the extracted values are never kept. Pass cursor: page for the next page.
195 196 197 |
# File 'lib/vision_api/client.rb', line 195 def requests(limit: nil, cursor: nil, timeout: nil) request(:get, "/v1/requests", query: { "limit" => limit, "cursor" => cursor }, timeout: timeout) end |
#schema(name, timeout: nil) ⇒ Object
234 235 236 |
# File 'lib/vision_api/client.rb', line 234 def schema(name, timeout: nil) request(:get, "/v1/schemas/#{escape_segment(name)}", timeout: timeout) end |
#schemas(timeout: nil) ⇒ Object
Every schema saved on this account.
230 231 232 |
# File 'lib/vision_api/client.rb', line 230 def schemas(timeout: nil) request(:get, "/v1/schemas", timeout: timeout)["data"] end |
#update_schema(name, preset: nil, schema: nil, timeout: nil) ⇒ Object
Replaces a saved schema in place.
249 250 251 252 253 254 |
# File 'lib/vision_api/client.rb', line 249 def update_schema(name, preset: nil, schema: nil, timeout: nil) body = {} body["preset"] = preset if preset body["schema"] = schema if schema request(:put, "/v1/schemas/#{escape_segment(name)}", json: body, timeout: timeout) end |
#wait_for_task(task_id, poll_interval: 2, max_wait: 600, raise_on_failure: true, on_poll: nil) ⇒ Object
Polls until the task leaves +queued+/+processing+, then returns it.
165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 |
# File 'lib/vision_api/client.rb', line 165 def wait_for_task(task_id, poll_interval: 2, max_wait: 600, raise_on_failure: true, on_poll: nil) deadline = monotonic + max_wait loop do task = get_task(task_id) case task["status"] when "completed" then return task when "failed" return task unless raise_on_failure raise TaskFailedError.new(task_id, task["error"]) end raise TaskTimeoutError.new(task_id, max_wait) if monotonic + poll_interval > deadline on_poll&.call(task) sleep(poll_interval) end end |