Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions src/http/apiCore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import { InputSource, PageOptions, LocalInputSource } from "@/input/index.js";
export const TIMEOUT_SECS_DEFAULT: number = 120;

export interface RequestOptions {
hostname: string;
path: string;
hostname?: string;
path?: string;
method: any;
timeoutSecs: number;
headers: any;
Expand All @@ -33,15 +33,15 @@ export async function cutDocPages(inputDoc: InputSource, pageOptions: PageOption
* Reads a response from the API and processes it.
* @param dispatcher custom dispatcher to use for the request.
* @param options options related to the request itself.
* @param url override the URL of the request.
* @returns the processed request.
*/
export async function sendRequestAndReadResponse(
dispatcher: Dispatcher,
options: RequestOptions,
url?: string,
): Promise<BaseHttpResponse> {
const url: string = `https://${options.hostname}${options.path}`;

logger.debug(`${options.method}: ${url}`);
url ??= `https://${options.hostname}${options.path}`;
const response = await request(
url,
{
Expand Down
22 changes: 10 additions & 12 deletions src/v2/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,6 @@ import { BaseProduct } from "@/v2/product/baseProduct.js";

/**
* Options for the V2 Mindee Client.
*
* @category ClientV2
* @example
* const client = new MindeeClientV2({
* apiKey: "YOUR_API_KEY",
Expand All @@ -32,8 +30,6 @@ export interface ClientOptions {

/**
* Mindee Client V2 class that centralizes most basic operations.
*
* @category ClientV2
*/
export class Client {
/** Mindee V2 API handler. */
Expand Down Expand Up @@ -89,7 +85,6 @@ export class Client {
* @param product the product to retrieve.
* @param inferenceId id of the queue to poll.
* @typeParam T an extension of an `Inference`. Can be omitted as it will be inferred from the `productClass`.
* @category Asynchronous
* @returns a `Promise` containing the inference.
*/
async getResult<P extends typeof BaseProduct>(
Expand All @@ -99,7 +94,7 @@ export class Client {
logger.debug(
`Attempting to get inference with ID: ${inferenceId} using response type: ${product.name}`
);
return await this.mindeeApi.getProductResult(product, inferenceId);
return await this.mindeeApi.getProductResultById(product, inferenceId);
}

/**
Expand All @@ -108,7 +103,6 @@ export class Client {
*
* @param jobId id of the queue to poll.
* @typeParam T an extension of an `Inference`. Can be omitted as it will be inferred from the `productClass`.
* @category Asynchronous
* @returns a `Promise` containing a `Job`, which also contains a `Document` if the
* parsing is complete.
*/
Expand All @@ -126,7 +120,6 @@ export class Client {
*
* @param pollingOptions options for the polling loop, see {@link PollingOptions}.
* @typeParam T an extension of an `Inference`. Can be omitted as it will be inferred from the `productClass`.
* @category Synchronous
* @returns a `Promise` containing parsing results.
*/
async enqueueAndGetResult<P extends typeof BaseProduct>(
Expand Down Expand Up @@ -155,7 +148,7 @@ export class Client {
protected async pollForResult<P extends typeof BaseProduct>(
product: typeof BaseProduct,
pollingOptions: PollingOptions,
queueId: string,
jobId: string,
): Promise<InstanceType<P["responseClass"]>> {
logger.debug(
`Waiting ${pollingOptions.initialDelaySec} seconds before polling.`
Expand All @@ -166,15 +159,15 @@ export class Client {
pollingOptions.initialTimerOptions
);
logger.debug(
`Start polling for inference using job ID: ${queueId}.`
`Start polling for inference using job ID: ${jobId}.`
);
let retryCounter: number = 1;
let pollResults: JobResponse;
while (retryCounter < pollingOptions.maxRetries + 1) {
logger.debug(
`Attempt ${retryCounter} of ${pollingOptions.maxRetries}`
);
pollResults = await this.getJob(queueId);
pollResults = await this.getJob(jobId);
const error: ErrorResponse | undefined = pollResults.job.error;
if (error) {
throw new MindeeHttpErrorV2(error);
Expand All @@ -184,7 +177,12 @@ export class Client {
break;
}
if (pollResults.job.status === "Processed") {
return this.getResult(product, pollResults.job.id);
if (!pollResults.job.resultUrl) {
throw new MindeeError(
"The result URL is undefined. This is a server error, try again later or contact support."
);
}
return this.mindeeApi.getProductResultByUrl(product, pollResults.job.resultUrl);
}
await setTimeout(
pollingOptions.delaySec * 1000,
Expand Down
3 changes: 3 additions & 0 deletions src/v2/http/apiSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ const API_V2_KEY_ENVVAR_NAME: string = "MINDEE_V2_API_KEY";
const API_V2_HOST_ENVVAR_NAME: string = "MINDEE_V2_API_HOST";
const DEFAULT_MINDEE_API_HOST: string = "api-v2.mindee.net";

/**
* Settings for the V2 API.
*/
export class ApiSettings extends BaseSettings {
baseHeaders: Record<string, string>;

Expand Down
64 changes: 38 additions & 26 deletions src/v2/http/mindeeApiV2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,14 @@ import {
RequestOptions
} from "@/http/apiCore.js";
import { InputSource, LocalInputSource, UrlInput } from "@/input/index.js";
import { MindeeDeserializationError } from "@/errors/index.js";
import { MindeeDeserializationError, MindeeError } from "@/errors/index.js";
import { MindeeHttpErrorV2 } from "./errors.js";
import { logger } from "@/logger.js";
import { BaseProduct } from "@/v2/product/baseProduct.js";


/**
* Mindee V2 API handler.
*/
export class MindeeApiV2 {
settings: ApiSettings;

Expand All @@ -27,12 +29,11 @@ export class MindeeApiV2 {
}

/**
* Sends a file to the extraction inference queue.
* Sends a file to the product inference queue.
* @param product product to enqueue.
* @param inputSource Local file loaded as an input.
* @param params {ExtractionParameters} parameters relating to the enqueueing options.
* @category V2
* @throws Error if the server's response contains one.
* @throws Error if the server's response contains an error.
* @returns a `Promise` containing a job response.
*/
async enqueueProduct(
Expand All @@ -51,35 +52,48 @@ export class MindeeApiV2 {
}

/**
* Requests the results of a queued document from the API.
* Throws an error if the server's response contains one.
* @param jobId The document's ID in the queue.
* @category Asynchronous
* @returns a `Promise` containing information on the queue.
* Get the specified Job.
* Throws an error if the server's response contains an error.
* @param jobId The Job ID as returned by the enqueue request.
* @returns a `Promise` containing the job response.
*/
async getJob(jobId: string): Promise<JobResponse> {
const response = await this.#reqGetJob(jobId);
return this.#processResponse(response, JobResponse);
}

/**
* Requests the job of a queued document from the API.
* Throws an error if the server's response contains one.
* Get the result of a queued document from the API.
* Throws an error if the server's response contains an error.
* @param product
* @param inferenceId The document's ID in the queue.
* @category Asynchronous
* @returns a `Promise` containing either the parsed result, or information on the queue.
* @param inferenceId The inference ID for the result.
* @returns a `Promise` containing the parsed result.
*/
async getProductResult<P extends typeof BaseProduct>(
async getProductResultById<P extends typeof BaseProduct>(
product: P,
inferenceId: string,
): Promise<InstanceType<P["responseClass"]>> {
const queueResponse: BaseHttpResponse = await this.#reqGetProductResult(
inferenceId, product.slug
`https://${this.settings.hostname}/v2/products/${product.slug}/results/${inferenceId}`
);
return this.#processResponse(queueResponse, product.responseClass) as InstanceType<P["responseClass"]>;
}

/**
* Get the result of a queued document from the API.
* Throws an error if the server's response contains an error.
* @param product
* @param url The URL as returned by a Job's resultUrl property.
* @returns a `Promise` containing the parsed result.
*/
async getProductResultByUrl<P extends typeof BaseProduct>(
product: P,
url: string,
): Promise<InstanceType<P["responseClass"]>> {
const queueResponse: BaseHttpResponse = await this.#reqGetProductResult(url);
return this.#processResponse(queueResponse, product.responseClass) as InstanceType<P["responseClass"]>;
}

#processResponse<T extends BaseResponse>(
result: BaseHttpResponse,
responseClass: ResponseConstructor<T>,
Expand Down Expand Up @@ -114,7 +128,6 @@ export class MindeeApiV2 {

/**
* Sends a document to the inference queue.
*
* @param product Product to enqueue.
* @param inputSource Local or remote file as an input.
* @param params {ExtractionParameters} parameters relating to the enqueueing options.
Expand Down Expand Up @@ -155,19 +168,18 @@ export class MindeeApiV2 {

/**
* Make a request to GET the status of a document in the queue.
* @param inferenceId ID of the inference.
* @param slug "jobs" or "inferences"...
* @category Asynchronous
* @returns a `Promise` containing either the parsed result, or information on the queue.
* @param url URL path to the result.
* @returns a `Promise` containing the parsed result.
*/
async #reqGetProductResult(inferenceId: string, slug: string): Promise<BaseHttpResponse> {
async #reqGetProductResult(url: string): Promise<BaseHttpResponse> {
const options: RequestOptions = {
method: "GET",
headers: this.settings.baseHeaders,
hostname: this.settings.hostname,
path: `/v2/products/${slug}/results/${inferenceId}`,
timeoutSecs: this.settings.timeoutSecs,
};
return await sendRequestAndReadResponse(this.settings.dispatcher, options);
if (!url.startsWith("https://")) {
throw new MindeeError(`Invalid URL: ${url}`);
}
return await sendRequestAndReadResponse(this.settings.dispatcher, options, url);
}
}
45 changes: 37 additions & 8 deletions tests/v2/client/client.integration.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { before, beforeEach, describe, it } from "node:test";
import assert from "node:assert/strict";
import path from "node:path";

Expand Down Expand Up @@ -37,7 +38,7 @@ function checkEmptyActiveOptions(inference: ExtractionInference) {
assert.equal(inference.activeOptions?.textContext, false);
}

describe("MindeeV2 – Integration - Client", () => {
describe("MindeeV2 – Integration - Client", { timeout: 120000 }, () => {
let client: Client;
let modelId: string;

Expand Down Expand Up @@ -98,7 +99,7 @@ describe("MindeeV2 – Integration - Client", () => {
assert.ok(inference.result);
assert.equal(inference.result.rawText, undefined);
checkEmptyActiveOptions(inference);
}).timeout(60000);
});

it("enqueueAndGetResult must succeed: Filled, single-page image – PathInput", async () => {
const source = new PathInput({ inputPath: sampleImagePath });
Expand Down Expand Up @@ -137,7 +138,7 @@ describe("MindeeV2 – Integration - Client", () => {
assert.equal(inference.activeOptions?.textContext, true);

assert.equal(inference.result.rawText?.pages.length, 1);
}).timeout(120000);
});

it("enqueueAndGetResult must succeed: Filled, single-page image – Base64Input", async () => {
const data = fs.readFileSync(sampleBase64Path, "utf8");
Expand Down Expand Up @@ -168,7 +169,7 @@ describe("MindeeV2 – Integration - Client", () => {
assert.equal(supplierField.value, "Clachan");

checkEmptyActiveOptions(inference);
}).timeout(120000);
});

it("enqueue must raise 422: Invalid model ID", async () => {
const source = new PathInput({ inputPath: emptyPdfPath });
Expand All @@ -180,7 +181,7 @@ describe("MindeeV2 – Integration - Client", () => {
} catch (err) {
check422(err);
}
}).timeout(60000);
});

it("getResult must raise 422: Invalid job ID", async () => {
try {
Expand All @@ -192,7 +193,35 @@ describe("MindeeV2 – Integration - Client", () => {
} catch (err) {
check422(err);
}
}).timeout(60000);
});

it("enqueue, getJob, and getResult must succeed", async () => {
const source = new PathInput({ inputPath: emptyPdfPath });
const params = {
modelId,
rag: false,
rawText: false,
polygon: false,
confidence: false,
alias: "ts_integration_all_together"
};

const enqueueResponse = await client.enqueue(
Extraction, source, params
);
assert.ok(enqueueResponse.job.id);

setTimeout(async () => {
const jobResponse = await client.getJob(enqueueResponse.job.id);
assert.ok(jobResponse.job.resultUrl);

const resultId = jobResponse.job.resultUrl?.split("/").pop() || "";
const resultResponse = await client.getResult(
Extraction, resultId
);
assert.strictEqual(resultId, resultResponse.inference.id);
}, 6500);
});

it("enqueueAndGetResult must succeed: HTTPS URL", async () => {
const url = process.env.MINDEE_V2_SE_TESTS_BLANK_PDF_URL ?? "error-no-url-found";
Expand All @@ -211,7 +240,7 @@ describe("MindeeV2 – Integration - Client", () => {
);
assert.ok(response);
assert.ok(response.inference instanceof ExtractionInference);
}).timeout(60000);
});

it("should override the data schema successfully", async () => {
const source = new PathInput({ inputPath: emptyPdfPath });
Expand All @@ -233,6 +262,6 @@ describe("MindeeV2 – Integration - Client", () => {
assert.ok(response.inference.result.fields.get("test_replace"));
assert.equal((response.inference.result.fields.get("test_replace") as SimpleField).value, "a test value");

}).timeout(60000);
});

});