Skip to content

Commit 8d732f8

Browse files
authored
🐛 always use the provided result URL (#451)
1 parent 4f83106 commit 8d732f8

5 files changed

Lines changed: 93 additions & 51 deletions

File tree

src/http/apiCore.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ import { InputSource, PageOptions, LocalInputSource } from "@/input/index.js";
55
export const TIMEOUT_SECS_DEFAULT: number = 120;
66

77
export interface RequestOptions {
8-
hostname: string;
9-
path: string;
8+
hostname?: string;
9+
path?: string;
1010
method: any;
1111
timeoutSecs: number;
1212
headers: any;
@@ -33,15 +33,15 @@ export async function cutDocPages(inputDoc: InputSource, pageOptions: PageOption
3333
* Reads a response from the API and processes it.
3434
* @param dispatcher custom dispatcher to use for the request.
3535
* @param options options related to the request itself.
36+
* @param url override the URL of the request.
3637
* @returns the processed request.
3738
*/
3839
export async function sendRequestAndReadResponse(
3940
dispatcher: Dispatcher,
4041
options: RequestOptions,
42+
url?: string,
4143
): Promise<BaseHttpResponse> {
42-
const url: string = `https://${options.hostname}${options.path}`;
43-
44-
logger.debug(`${options.method}: ${url}`);
44+
url ??= `https://${options.hostname}${options.path}`;
4545
const response = await request(
4646
url,
4747
{

src/v2/client.ts

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,6 @@ import { BaseProduct } from "@/v2/product/baseProduct.js";
1212

1313
/**
1414
* Options for the V2 Mindee Client.
15-
*
16-
* @category ClientV2
1715
* @example
1816
* const client = new MindeeClientV2({
1917
* apiKey: "YOUR_API_KEY",
@@ -32,8 +30,6 @@ export interface ClientOptions {
3230

3331
/**
3432
* Mindee Client V2 class that centralizes most basic operations.
35-
*
36-
* @category ClientV2
3733
*/
3834
export class Client {
3935
/** Mindee V2 API handler. */
@@ -89,7 +85,6 @@ export class Client {
8985
* @param product the product to retrieve.
9086
* @param inferenceId id of the queue to poll.
9187
* @typeParam T an extension of an `Inference`. Can be omitted as it will be inferred from the `productClass`.
92-
* @category Asynchronous
9388
* @returns a `Promise` containing the inference.
9489
*/
9590
async getResult<P extends typeof BaseProduct>(
@@ -99,7 +94,7 @@ export class Client {
9994
logger.debug(
10095
`Attempting to get inference with ID: ${inferenceId} using response type: ${product.name}`
10196
);
102-
return await this.mindeeApi.getProductResult(product, inferenceId);
97+
return await this.mindeeApi.getProductResultById(product, inferenceId);
10398
}
10499

105100
/**
@@ -108,7 +103,6 @@ export class Client {
108103
*
109104
* @param jobId id of the queue to poll.
110105
* @typeParam T an extension of an `Inference`. Can be omitted as it will be inferred from the `productClass`.
111-
* @category Asynchronous
112106
* @returns a `Promise` containing a `Job`, which also contains a `Document` if the
113107
* parsing is complete.
114108
*/
@@ -126,7 +120,6 @@ export class Client {
126120
*
127121
* @param pollingOptions options for the polling loop, see {@link PollingOptions}.
128122
* @typeParam T an extension of an `Inference`. Can be omitted as it will be inferred from the `productClass`.
129-
* @category Synchronous
130123
* @returns a `Promise` containing parsing results.
131124
*/
132125
async enqueueAndGetResult<P extends typeof BaseProduct>(
@@ -155,7 +148,7 @@ export class Client {
155148
protected async pollForResult<P extends typeof BaseProduct>(
156149
product: typeof BaseProduct,
157150
pollingOptions: PollingOptions,
158-
queueId: string,
151+
jobId: string,
159152
): Promise<InstanceType<P["responseClass"]>> {
160153
logger.debug(
161154
`Waiting ${pollingOptions.initialDelaySec} seconds before polling.`
@@ -166,15 +159,15 @@ export class Client {
166159
pollingOptions.initialTimerOptions
167160
);
168161
logger.debug(
169-
`Start polling for inference using job ID: ${queueId}.`
162+
`Start polling for inference using job ID: ${jobId}.`
170163
);
171164
let retryCounter: number = 1;
172165
let pollResults: JobResponse;
173166
while (retryCounter < pollingOptions.maxRetries + 1) {
174167
logger.debug(
175168
`Attempt ${retryCounter} of ${pollingOptions.maxRetries}`
176169
);
177-
pollResults = await this.getJob(queueId);
170+
pollResults = await this.getJob(jobId);
178171
const error: ErrorResponse | undefined = pollResults.job.error;
179172
if (error) {
180173
throw new MindeeHttpErrorV2(error);
@@ -184,7 +177,12 @@ export class Client {
184177
break;
185178
}
186179
if (pollResults.job.status === "Processed") {
187-
return this.getResult(product, pollResults.job.id);
180+
if (!pollResults.job.resultUrl) {
181+
throw new MindeeError(
182+
"The result URL is undefined. This is a server error, try again later or contact support."
183+
);
184+
}
185+
return this.mindeeApi.getProductResultByUrl(product, pollResults.job.resultUrl);
188186
}
189187
await setTimeout(
190188
pollingOptions.delaySec * 1000,

src/v2/http/apiSettings.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ const API_V2_KEY_ENVVAR_NAME: string = "MINDEE_V2_API_KEY";
66
const API_V2_HOST_ENVVAR_NAME: string = "MINDEE_V2_API_HOST";
77
const DEFAULT_MINDEE_API_HOST: string = "api-v2.mindee.net";
88

9+
/**
10+
* Settings for the V2 API.
11+
*/
912
export class ApiSettings extends BaseSettings {
1013
baseHeaders: Record<string, string>;
1114

src/v2/http/mindeeApiV2.ts

Lines changed: 38 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,14 @@ import {
1313
RequestOptions
1414
} from "@/http/apiCore.js";
1515
import { InputSource, LocalInputSource, UrlInput } from "@/input/index.js";
16-
import { MindeeDeserializationError } from "@/errors/index.js";
16+
import { MindeeDeserializationError, MindeeError } from "@/errors/index.js";
1717
import { MindeeHttpErrorV2 } from "./errors.js";
1818
import { logger } from "@/logger.js";
1919
import { BaseProduct } from "@/v2/product/baseProduct.js";
2020

21-
21+
/**
22+
* Mindee V2 API handler.
23+
*/
2224
export class MindeeApiV2 {
2325
settings: ApiSettings;
2426

@@ -27,12 +29,11 @@ export class MindeeApiV2 {
2729
}
2830

2931
/**
30-
* Sends a file to the extraction inference queue.
32+
* Sends a file to the product inference queue.
3133
* @param product product to enqueue.
3234
* @param inputSource Local file loaded as an input.
3335
* @param params {ExtractionParameters} parameters relating to the enqueueing options.
34-
* @category V2
35-
* @throws Error if the server's response contains one.
36+
* @throws Error if the server's response contains an error.
3637
* @returns a `Promise` containing a job response.
3738
*/
3839
async enqueueProduct(
@@ -51,35 +52,48 @@ export class MindeeApiV2 {
5152
}
5253

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

6565
/**
66-
* Requests the job of a queued document from the API.
67-
* Throws an error if the server's response contains one.
66+
* Get the result of a queued document from the API.
67+
* Throws an error if the server's response contains an error.
6868
* @param product
69-
* @param inferenceId The document's ID in the queue.
70-
* @category Asynchronous
71-
* @returns a `Promise` containing either the parsed result, or information on the queue.
69+
* @param inferenceId The inference ID for the result.
70+
* @returns a `Promise` containing the parsed result.
7271
*/
73-
async getProductResult<P extends typeof BaseProduct>(
72+
async getProductResultById<P extends typeof BaseProduct>(
7473
product: P,
7574
inferenceId: string,
7675
): Promise<InstanceType<P["responseClass"]>> {
7776
const queueResponse: BaseHttpResponse = await this.#reqGetProductResult(
78-
inferenceId, product.slug
77+
`https://${this.settings.hostname}/v2/products/${product.slug}/results/${inferenceId}`
7978
);
8079
return this.#processResponse(queueResponse, product.responseClass) as InstanceType<P["responseClass"]>;
8180
}
8281

82+
/**
83+
* Get the result of a queued document from the API.
84+
* Throws an error if the server's response contains an error.
85+
* @param product
86+
* @param url The URL as returned by a Job's resultUrl property.
87+
* @returns a `Promise` containing the parsed result.
88+
*/
89+
async getProductResultByUrl<P extends typeof BaseProduct>(
90+
product: P,
91+
url: string,
92+
): Promise<InstanceType<P["responseClass"]>> {
93+
const queueResponse: BaseHttpResponse = await this.#reqGetProductResult(url);
94+
return this.#processResponse(queueResponse, product.responseClass) as InstanceType<P["responseClass"]>;
95+
}
96+
8397
#processResponse<T extends BaseResponse>(
8498
result: BaseHttpResponse,
8599
responseClass: ResponseConstructor<T>,
@@ -114,7 +128,6 @@ export class MindeeApiV2 {
114128

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

156169
/**
157170
* Make a request to GET the status of a document in the queue.
158-
* @param inferenceId ID of the inference.
159-
* @param slug "jobs" or "inferences"...
160-
* @category Asynchronous
161-
* @returns a `Promise` containing either the parsed result, or information on the queue.
171+
* @param url URL path to the result.
172+
* @returns a `Promise` containing the parsed result.
162173
*/
163-
async #reqGetProductResult(inferenceId: string, slug: string): Promise<BaseHttpResponse> {
174+
async #reqGetProductResult(url: string): Promise<BaseHttpResponse> {
164175
const options: RequestOptions = {
165176
method: "GET",
166177
headers: this.settings.baseHeaders,
167-
hostname: this.settings.hostname,
168-
path: `/v2/products/${slug}/results/${inferenceId}`,
169178
timeoutSecs: this.settings.timeoutSecs,
170179
};
171-
return await sendRequestAndReadResponse(this.settings.dispatcher, options);
180+
if (!url.startsWith("https://")) {
181+
throw new MindeeError(`Invalid URL: ${url}`);
182+
}
183+
return await sendRequestAndReadResponse(this.settings.dispatcher, options, url);
172184
}
173185
}

tests/v2/client/client.integration.ts

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { before, beforeEach, describe, it } from "node:test";
12
import assert from "node:assert/strict";
23
import path from "node:path";
34

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

40-
describe("MindeeV2 – Integration - Client", () => {
41+
describe("MindeeV2 – Integration - Client", { timeout: 120000 }, () => {
4142
let client: Client;
4243
let modelId: string;
4344

@@ -98,7 +99,7 @@ describe("MindeeV2 – Integration - Client", () => {
9899
assert.ok(inference.result);
99100
assert.equal(inference.result.rawText, undefined);
100101
checkEmptyActiveOptions(inference);
101-
}).timeout(60000);
102+
});
102103

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

139140
assert.equal(inference.result.rawText?.pages.length, 1);
140-
}).timeout(120000);
141+
});
141142

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

170171
checkEmptyActiveOptions(inference);
171-
}).timeout(120000);
172+
});
172173

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

185186
it("getResult must raise 422: Invalid job ID", async () => {
186187
try {
@@ -192,7 +193,35 @@ describe("MindeeV2 – Integration - Client", () => {
192193
} catch (err) {
193194
check422(err);
194195
}
195-
}).timeout(60000);
196+
});
197+
198+
it("enqueue, getJob, and getResult must succeed", async () => {
199+
const source = new PathInput({ inputPath: emptyPdfPath });
200+
const params = {
201+
modelId,
202+
rag: false,
203+
rawText: false,
204+
polygon: false,
205+
confidence: false,
206+
alias: "ts_integration_all_together"
207+
};
208+
209+
const enqueueResponse = await client.enqueue(
210+
Extraction, source, params
211+
);
212+
assert.ok(enqueueResponse.job.id);
213+
214+
setTimeout(async () => {
215+
const jobResponse = await client.getJob(enqueueResponse.job.id);
216+
assert.ok(jobResponse.job.resultUrl);
217+
218+
const resultId = jobResponse.job.resultUrl?.split("/").pop() || "";
219+
const resultResponse = await client.getResult(
220+
Extraction, resultId
221+
);
222+
assert.strictEqual(resultId, resultResponse.inference.id);
223+
}, 6500);
224+
});
196225

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

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

236-
}).timeout(60000);
265+
});
237266

238267
});

0 commit comments

Comments
 (0)