diff --git a/lib/mindee/v2/client.rb b/lib/mindee/v2/client.rb index 0b06d82a..324d5a39 100644 --- a/lib/mindee/v2/client.rb +++ b/lib/mindee/v2/client.rb @@ -18,22 +18,7 @@ def initialize(api_key: '') @mindee_api = Mindee::V2::HTTP::MindeeApiV2.new(api_key: api_key) end - # Retrieves a result from a given queue or URL to the result. - # @param product [Class] The return class. - # @param resource [String] ID of the inference or URL to the result. - # @return [Mindee::V2::Parsing::BaseResponse] - def get_result(product, resource) - @mindee_api.req_get_result(product, resource) - end - - # Retrieves an inference from a given queue or URL to the job. - # @param job_id [String] ID of the job. - # @return [Mindee::V2::Parsing::JobResponse] - def get_job(job_id) - @mindee_api.req_get_job(job_id) - end - - # Enqueue a document for async parsing. + # Send a file to the asynchronous processing queue for inference. # @param product [Class] The return class. # @param input_source [Mindee::Input::Source::LocalInputSource, Mindee::Input::Source::URLInputSource] # The source of the input document (local file or URL). @@ -51,8 +36,40 @@ def enqueue( @mindee_api.req_post_product_enqueue(input_source, normalized_params) end - # Enqueues to an asynchronous endpoint and automatically polls for a response. - # + # Get the status of an inference that was previously enqueued. + # Can be used for polling. + # @param url [String] HTTPS URL to the job. + # @return [Mindee::V2::Parsing::JobResponse] + def get_job_by_url(url) + @mindee_api.req_get_job_by_url(url) + end + + # Get the result of an inference that was previously enqueued. + # @param product [Class] The return class. + # @param url [String] HTTPS URL to the result. + # @return [Mindee::V2::Parsing::BaseResponse] + def get_result_by_url(product, url) + @mindee_api.req_get_result_by_url(product, url) + end + + # Get the result of an inference that was previously enqueued. + # Prefer using `get_result_by_url` using the result URL returned by the Job. + # @param product [Class] The return class. + # @param resource [String] ID of the inference or URL to the result. + # @return [Mindee::V2::Parsing::BaseResponse] + def get_result(product, resource) + @mindee_api.req_get_result_by_id(product, resource) + end + + # Get the status of an inference that was previously enqueued. + # Prefer using `get_job_by_url` with the polling URL returned by enqueue. + # @param job_id [String] ID of the job. + # @return [Mindee::V2::Parsing::JobResponse] + def get_job(job_id) + @mindee_api.req_get_job_by_id(job_id) + end + + # Send a file to the inference queue and return the results when complete. # @param product [Class] The return class. # @param input_source [Mindee::Input::Source::LocalInputSource, Mindee::Input::Source::URLInputSource] # The source of the input document (local file or URL). @@ -60,7 +77,7 @@ def enqueue( # @param polling_options [Hash, PollingOptions, nil] Parameters for polling. # @param cancellation_token [Mindee::HTTP::CancellationToken, nil] Token for cancellation. # @return [Parsing::BaseResponse] - # rubocop:disable-next Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity + # rubocop:disable-next Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Metrics/AbcSize def enqueue_and_get_result( product, input_source, @@ -84,29 +101,31 @@ def enqueue_and_get_result( sleep(normalized_params.polling_options.initial_delay_sec) retry_counter = 1 - poll_results = get_job(job_id) + polling_url = enqueue_response.job.polling_url + + job_response = get_job_by_url(polling_url) while retry_counter < normalized_params.polling_options.max_retries - if poll_results.job.status == 'Failed' + if job_response.job.status == 'Failed' break - elsif !poll_results.job.result_url.nil? - return get_result(product, poll_results.job.result_url) + elsif !job_response.job.result_url.nil? + return get_result_by_url(product, job_response.job.result_url) end logger.debug( "Successfully enqueued inference with job id: #{job_id}.\n" \ "Attempt n°#{retry_counter}/#{normalized_params.polling_options.max_retries}.\n" \ - "Job status: #{poll_results.job.status}." + "Job status: #{job_response.job.status}." ) raise Mindee::Error::MindeeError, 'Enqueueing of the document was canceled.' if cancellation_token&.canceled? sleep(normalized_params.polling_options.delay_sec) - poll_results = get_job(job_id) + job_response = get_job_by_url(polling_url) retry_counter += 1 end - error = poll_results.job.error + error = job_response.job.error unless error.nil? err_to_raise = Mindee::Error::MindeeHTTPErrorV2.new(error) # NOTE: purposefully decoupled from the line above, otherwise rubocop thinks `error` is a `message` param. diff --git a/lib/mindee/v2/http/mindee_api_v2.rb b/lib/mindee/v2/http/mindee_api_v2.rb index ba7ccf79..dc9e72cd 100644 --- a/lib/mindee/v2/http/mindee_api_v2.rb +++ b/lib/mindee/v2/http/mindee_api_v2.rb @@ -33,30 +33,44 @@ def req_post_product_enqueue(input_source, params) end # Retrieves a result from a given queue. - # @param product [Class] The return class. - # @param resource [String] ID of the inference or URL to the result. + # @param product [Class] + # @param inference_id [String] ID of the inference or URL to the result. # @return [Mindee::V2::Parsing::BaseResponse] - def req_get_result(product, resource) - return req_get_result_url(product.response_type, resource) if uri?(resource) - - @settings.check_api_key - response = result_req_get( - resource, - product - ) - product.response_type.new(process_response(response)) + def req_get_result_by_id(product, inference_id) + req_get_result_by_url(product, "#{@settings.base_url}/v2/products/#{product.slug}/results/#{inference_id}") end # Retrieves a queued job. # # @param job_id [String] ID of the job or URL to the job. # @return [Mindee::V2::Parsing::JobResponse] - def req_get_job(job_id) + def req_get_job_by_id(job_id) + req_get_job_by_url("#{@settings.base_url}/v2/jobs/#{job_id}") + end + + # Retrieves a queued job from its polling URL. + # + # @param url [String] HTTPS URL to the job. + # @return [Mindee::V2::Parsing::JobResponse] + def req_get_job_by_url(url) + validate_https_url!(url) @settings.check_api_key - response = poll("#{@settings.base_url}/v2/jobs/#{job_id}") + response = poll(url) Mindee::V2::Parsing::JobResponse.new(process_response(response)) end + # Retrieves a result from its URL. + # + # @param product [Class] + # @param url [String] HTTPS URL to the result. + # @return [Mindee::V2::Parsing::BaseResponse] + def req_get_result_by_url(product, url) + validate_https_url!(url) + @settings.check_api_key + response = poll(url) + product.response_type.new(process_response(response)) + end + # Retrieves a list of resources with the given criteria. # @param params [ClientOptions::BaseSearchParameters] Search parameters. # @return [Mindee::V2::Parsing::Search::BaseSearchResponse] A search response containing the matching @@ -85,35 +99,15 @@ def req_get_search(params) private - # @param resource [String] Resource to check. - # @return [Boolean] - def uri?(resource) - uri = URI.parse(resource) - throw Mindee::Error::MindeeError, 'HTTP is not supported.' if uri.scheme == 'http' - uri.scheme == 'https' - rescue URI::BadURIError, URI::InvalidURIError - false - end - - # Retrieves a queued job. - # - # @param url [String] - # @return [Mindee::V2::Parsing::JobResponse] - def req_get_job_url(url) - @settings.check_api_key - response = poll(url) - Mindee::V2::Parsing::JobResponse.new(process_response(response)) - end - - # Retrieves a queued job. + # Validates that a URL uses HTTPS and includes a host. # - # @param result_class [Class] # @param url [String] - # @return [Mindee::V2::Parsing::BaseResponse] - def req_get_result_url(result_class, url) - @settings.check_api_key - response = poll(url) - result_class.new(process_response(response)) + # @return [void] + def validate_https_url!(url) + uri = URI.parse(url) + raise Mindee::Error::MindeeError, "Invalid URL: #{url}" unless uri.scheme == 'https' && !uri.host.to_s.empty? + rescue URI::BadURIError, URI::InvalidURIError + raise Mindee::Error::MindeeError, "Invalid URL: #{url}" end # Converts an HTTP response to a parsed response object. @@ -156,15 +150,6 @@ def poll(url) raise Mindee::Error::MindeeError, 'Could not resolve server response.' end - # Polls the API for the result of an inference. - # - # @param queue_id [String] ID of the queue. - # @param product [Class] The return class. - # @return [Net::HTTPResponse] - def result_req_get(queue_id, product) - poll("#{@settings.base_url}/v2/products/#{product.slug}/results/#{queue_id}") - end - # Handle parameters for the enqueue form # @param form_data [Array] Array of form fields # @param params [V2::Product::Extraction::Params::ExtractionParameters] Inference options. diff --git a/sig/mindee/v2/client.rbs b/sig/mindee/v2/client.rbs index 57a6150b..2516936d 100644 --- a/sig/mindee/v2/client.rbs +++ b/sig/mindee/v2/client.rbs @@ -15,6 +15,10 @@ module Mindee def get_job: (String job_id) -> V2::Parsing::JobResponse + def get_result_by_url: [T] (HTTP::_ProductClass[T] product, String url) -> T + + def get_job_by_url: (String url) -> V2::Parsing::JobResponse + def enqueue: [T] (HTTP::_ProductClass[T] product, Input::Source::LocalInputSource | Input::Source::URLInputSource, Hash[String | Symbol, untyped] | ClientOptions::BaseProductParameters params) -> V2::Parsing::JobResponse def enqueue_and_get_result: [T] (HTTP::_ProductClass[T] product, Input::Source::LocalInputSource | Input::Source::URLInputSource, Hash[String | Symbol, untyped] | ClientOptions::BaseProductParameters params, ?Hash[String | Symbol, untyped] | Input::PollingOptions | nil) -> T diff --git a/sig/mindee/v2/http/mindee_api_v2.rbs b/sig/mindee/v2/http/mindee_api_v2.rbs index 534f6564..04d4e7b3 100644 --- a/sig/mindee/v2/http/mindee_api_v2.rbs +++ b/sig/mindee/v2/http/mindee_api_v2.rbs @@ -27,18 +27,20 @@ module Mindee def initialize: (?api_key: String?) -> void - def req_get_result: [T] (_ProductClass[T] product, String resource) -> T + def req_get_result_by_id: [T] (_ProductClass[T] product, String inference_id) -> T def req_post_product_enqueue: (Input::Source::LocalInputSource | Input::Source::URLInputSource, ClientOptions::BaseProductParameters) -> V2::Parsing::JobResponse - def req_get_job: (String) -> V2::Parsing::JobResponse + def req_get_job_by_id: (String job_id) -> V2::Parsing::JobResponse + + def req_get_job_by_url: (String url) -> V2::Parsing::JobResponse + + def req_get_result_by_url: [T] (_ProductClass[T] product, String url) -> T def process_response: (Net::HTTPResponse?) -> Hash[String | Symbol, untyped] def poll: (String) -> Net::HTTPResponse - def result_req_get: [T] (String, _ProductClass[T] product) -> Net::HTTPResponse - def enqueue: (Input::Source::LocalInputSource | Input::Source::URLInputSource, ClientOptions::BaseProductParameters) -> Net::HTTPResponse? def req_get_search: [T] (_SearchParameters[T] params) -> T @@ -47,11 +49,7 @@ module Mindee def enqueue_form_options: (Array[Array[untyped]], V2::Product::Extraction::Params::ExtractionParameters) -> Array[Array[untyped]] - def req_get_job_url: (String) -> V2::Parsing::JobResponse - - def req_get_result_url: [T] (_ResponseFactory[T] result_class, String url) -> T - - def uri?: (String) -> bool + def validate_https_url!: (String) -> void end end end diff --git a/spec/v2/client_v2_spec.rb b/spec/v2/client_v2_spec.rb index 13e0be0c..495b777d 100644 --- a/spec/v2/client_v2_spec.rb +++ b/spec/v2/client_v2_spec.rb @@ -119,6 +119,7 @@ def stub_next_request_with(method, hash:, status_code: 0) resp.job.completed_at.strftime('%Y-%m-%dT%H:%M:%S.%6N') ).to eq('2026-04-20T18:32:02.734312') end + it 'poll builds a real Net::HTTP::Get request without a Transfer-Encoding header' do job_json = File.read(File.join(V2_DATA_DIR, 'job', 'ok_processing.json')) captured_request = nil @@ -161,7 +162,7 @@ def stub_next_request_with(method, hash:, status_code: 0) before do allow(client).to receive(:sleep) allow(client).to receive(:enqueue).and_return(mock_enqueue_res) - allow(client).to receive(:get_job).and_return(mock_processing_res) + allow(client).to receive(:get_job_by_url).and_return(mock_processing_res) end it 'raises MindeeError when token is cancelled before first poll' do @@ -178,7 +179,7 @@ def stub_next_request_with(method, hash:, status_code: 0) it 'raises MindeeError when token is cancelled during poll loop' do token = Mindee::HTTP::CancellationToken.new call_count = 0 - allow(client).to receive(:get_job) do + allow(client).to receive(:get_job_by_url) do call_count += 1 token.cancel if call_count == 1 mock_processing_res