Skip to content

Commit

Permalink
Add consistent LRO operation fail behavior across Form Recognizer pol…
Browse files Browse the repository at this point in the history
…ler methods (#11670)
  • Loading branch information
samvaity authored Jun 3, 2020
1 parent 5535278 commit f29e0da
Show file tree
Hide file tree
Showing 15 changed files with 630 additions and 189 deletions.
4 changes: 4 additions & 0 deletions sdk/formrecognizer/azure-ai-formrecognizer/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
# Release History

## 1.0.0-beta.3 (Unreleased)
- Rename parameters data and sourceUrl parameters found on methods for FormRecognizerClient to form and formUrl, respectively.
- Rename parameters for receipt API methods to receipt and receiptUrl.
- Raise `HttpResponseException` when a model with `ModelStatus.Invalid` is returned from the `beginTraining()` API's
- Fix `HttpResponseException` to include the error object thrown on invalid analyze status for recognize API's
- Update FormField property `transactionTime` on `USReceipt` to return `LocalTime` instead of `String`
- Rename model `PageRange` to `FormPageRange`
- Rename property `startPageNumber` to `firstPageNumber` and `endPageNumber` to `lastPageNumber` in model `PageRange`
Expand Down
2 changes: 1 addition & 1 deletion sdk/formrecognizer/azure-ai-formrecognizer/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ from form documents. It includes the following main functionalities:
```
[//]: # ({x-version-update-end})

### Create a Form Recognizer resource
#### Create a Form Recognizer resource
Form Recognizer supports both [multi-service and single-service access][service_access]. Create a Cognitive Service's
resource if you plan to access multiple cognitive services under a single endpoint/key. For Form Recognizer access only,
create a Form Recognizer resource.
Expand Down

Large diffs are not rendered by default.

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@
import com.azure.ai.formrecognizer.implementation.models.CopyRequest;
import com.azure.ai.formrecognizer.implementation.models.Model;
import com.azure.ai.formrecognizer.implementation.models.OperationStatus;
import com.azure.ai.formrecognizer.implementation.models.ModelInfo;
import com.azure.ai.formrecognizer.implementation.models.ModelStatus;
import com.azure.ai.formrecognizer.implementation.models.TrainRequest;
import com.azure.ai.formrecognizer.implementation.models.TrainSourceFilter;
import com.azure.ai.formrecognizer.models.AccountProperties;
Expand Down Expand Up @@ -127,10 +129,12 @@ String getEndpoint() {
*
* @param trainingFilesUrl source URL parameter that is either an externally accessible Azure
* storage blob container Uri (preferably a Shared Access Signature Uri).
* @param useTrainingLabels Boolean to specify the use of labeled files for training the model.
* @param useTrainingLabels boolean to specify the use of labeled files for training the model.
*
* @return A {@link PollerFlux} that polls the training model operation until it has completed, has failed, or has
* been cancelled.
* been cancelled. The completed operation returns a {@link CustomFormModel}.
* @throws HttpResponseException If training fails and model with {@link ModelStatus#INVALID} is created.
* @throws NullPointerException If {@code trainingFilesUrl} is {@code null}.
*/
@ServiceMethod(returns = ReturnType.SINGLE)
public PollerFlux<OperationResult, CustomFormModel> beginTraining(String trainingFilesUrl,
Expand All @@ -141,8 +145,7 @@ public PollerFlux<OperationResult, CustomFormModel> beginTraining(String trainin
/**
* Create and train a custom model.
* <p>Models are trained using documents that are of the following content type -
* 'application/pdf', 'image/jpeg', 'image/png', 'image/tiff'.
* Other type of content is ignored.
* 'application/pdf', 'image/jpeg', 'image/png', 'image/tiff'.Other type of content is ignored.
* </p>
* <p>The service does not support cancellation of the long running operation and returns with an
* error message indicating absence of cancellation support.</p>
Expand All @@ -152,13 +155,15 @@ public PollerFlux<OperationResult, CustomFormModel> beginTraining(String trainin
*
* @param trainingFilesUrl an externally accessible Azure storage blob container Uri (preferably a
* Shared Access Signature Uri).
* @param useTrainingLabels Boolean to specify the use of labeled files for training the model.
* @param useTrainingLabels boolean to specify the use of labeled files for training the model.
* @param trainingFileFilter Filter to apply to the documents in the source path for training.
* @param pollInterval Duration between each poll for the operation status. If none is specified, a default of
* 5 seconds is used.
*
* @return A {@link PollerFlux} that polls the extract receipt operation until it
* has completed, has failed, or has been cancelled.
* has completed, has failed, or has been cancelled. The completed operation returns a {@link CustomFormModel}.
* @throws HttpResponseException If training fails and model with {@link ModelStatus#INVALID} is created.
* @throws NullPointerException If {@code trainingFilesUrl} is {@code null}.
*/
@ServiceMethod(returns = ReturnType.SINGLE)
public PollerFlux<OperationResult, CustomFormModel> beginTraining(String trainingFilesUrl,
Expand All @@ -184,6 +189,7 @@ public PollerFlux<OperationResult, CustomFormModel> beginTraining(String trainin
* @param modelId The UUID string format model identifier.
*
* @return The detailed information for the specified model.
* @throws NullPointerException If {@code modelId} is {@code null}.
*/
@ServiceMethod(returns = ReturnType.SINGLE)
public Mono<CustomFormModel> getCustomModel(String modelId) {
Expand All @@ -199,6 +205,7 @@ public Mono<CustomFormModel> getCustomModel(String modelId) {
* @param modelId The UUID string format model identifier.
*
* @return A {@link Response} containing the requested {@link CustomFormModel model}.
* @throws NullPointerException If {@code modelId} is {@code null}.
*/
@ServiceMethod(returns = ReturnType.SINGLE)
public Mono<Response<CustomFormModel>> getCustomModelWithResponse(String modelId) {
Expand Down Expand Up @@ -261,6 +268,7 @@ Mono<Response<AccountProperties>> getAccountPropertiesWithResponse(Context conte
* @param modelId The UUID string format model identifier.
*
* @return An empty Mono.
* @throws NullPointerException If {@code modelId} is {@code null}.
*/
@ServiceMethod(returns = ReturnType.SINGLE)
public Mono<Void> deleteModel(String modelId) {
Expand All @@ -276,6 +284,7 @@ public Mono<Void> deleteModel(String modelId) {
* @param modelId The UUID string format model identifier.
*
* @return A {@link Mono} containing containing status code and HTTP headers
* @throws NullPointerException If {@code modelId} is {@code null}.
*/
@ServiceMethod(returns = ReturnType.SINGLE)
public Mono<Response<Void>> deleteModelWithResponse(String modelId) {
Expand Down Expand Up @@ -559,7 +568,10 @@ private Function<PollingContext<OperationResult>, Mono<CustomFormModel>> fetchTr
try {
final UUID modelUid = UUID.fromString(pollingContext.getLatestResponse().getValue().getResultId());
return service.getCustomModelWithResponseAsync(modelUid, true)
.map(modelSimpleResponse -> toCustomFormModel(modelSimpleResponse.getValue()));
.map(modelSimpleResponse -> {
throwIfModelStatusInvalid(modelSimpleResponse.getValue());
return toCustomFormModel(modelSimpleResponse.getValue());
});
} catch (RuntimeException ex) {
return monoError(logger, ex);
}
Expand All @@ -583,13 +595,13 @@ private Function<PollingContext<OperationResult>, Mono<CustomFormModel>> fetchTr
}

private Function<PollingContext<OperationResult>, Mono<OperationResult>> getTrainingActivationOperation(
String fileSourceUrl, boolean includeSubFolders, String filePrefix, boolean useTrainingLabels) {
String trainingFilesUrl, boolean includeSubFolders, String filePrefix, boolean useTrainingLabels) {
return (pollingContext) -> {
try {
Objects.requireNonNull(fileSourceUrl, "'fileSourceUrl' cannot be null.");
Objects.requireNonNull(trainingFilesUrl, "'trainingFilesUrl' cannot be null.");
TrainSourceFilter trainSourceFilter = new TrainSourceFilter().setIncludeSubFolders(includeSubFolders)
.setPrefix(filePrefix);
TrainRequest serviceTrainRequest = new TrainRequest().setSource(fileSourceUrl).
TrainRequest serviceTrainRequest = new TrainRequest().setSource(trainingFilesUrl).
setSourceFilter(trainSourceFilter).setUseLabelFile(useTrainingLabels);
return service.trainCustomModelAsyncWithResponseAsync(serviceTrainRequest)
.map(response ->
Expand Down Expand Up @@ -638,4 +650,20 @@ private void throwIfCopyOperationStatusInvalid(CopyOperationResult copyResult) {
}
}

/**
* Helper method that throws a {@link HttpResponseException} if {@link ModelInfo#getStatus()} is
* {@link com.azure.ai.formrecognizer.implementation.models.ModelStatus#INVALID}.
*
* @param customModel The response returned from the service.
*/
private void throwIfModelStatusInvalid(Model customModel) {
if (ModelStatus.INVALID.equals(customModel.getModelInfo().getStatus())) {
List<ErrorInformation> errorInformationList = customModel.getTrainResult().getErrors();
if (!CoreUtils.isNullOrEmpty(errorInformationList)) {
throw logger.logExceptionAsError(new HttpResponseException(
String.format("Invalid model created with ID: %s", customModel.getModelInfo().getModelId()),
null, errorInformationList));
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import com.azure.ai.formrecognizer.FormRecognizerClient;
import com.azure.ai.formrecognizer.FormRecognizerClientBuilder;
import com.azure.ai.formrecognizer.implementation.models.ModelStatus;
import com.azure.ai.formrecognizer.models.AccountProperties;
import com.azure.ai.formrecognizer.models.CopyAuthorization;
import com.azure.ai.formrecognizer.models.CustomFormModel;
Expand All @@ -14,6 +15,7 @@
import com.azure.core.annotation.ReturnType;
import com.azure.core.annotation.ServiceClient;
import com.azure.core.annotation.ServiceMethod;
import com.azure.core.exception.HttpResponseException;
import com.azure.core.http.rest.PagedIterable;
import com.azure.core.http.rest.Response;
import com.azure.core.util.Context;
Expand Down Expand Up @@ -73,10 +75,12 @@ public FormRecognizerClient getFormRecognizerClient() {
*
* @param trainingFilesUrl an externally accessible Azure storage blob container Uri (preferably a Shared Access
* Signature Uri).
* @param useTrainingLabels Boolean to specify the use of labeled files for training the model.
* @param useTrainingLabels boolean to specify the use of labeled files for training the model.
*
* @return A {@link SyncPoller} that polls the training model operation until it has completed, has failed, or has
* been cancelled. The completed operation returns a {@link CustomFormModel}.
* @throws HttpResponseException If training fails and model with {@link ModelStatus#INVALID} is created.
* @throws NullPointerException If {@code trainingFilesUrl} is {@code null}.
*/
@ServiceMethod(returns = ReturnType.SINGLE)
public SyncPoller<OperationResult, CustomFormModel> beginTraining(String trainingFilesUrl,
Expand All @@ -97,13 +101,15 @@ public SyncPoller<OperationResult, CustomFormModel> beginTraining(String trainin
*
* @param trainingFilesUrl an externally accessible Azure storage blob container Uri (preferably a
* Shared Access Signature Uri).
* @param useTrainingLabels Boolean to specify the use of labeled files for training the model.
* @param useTrainingLabels boolean to specify the use of labeled files for training the model.
* @param trainingFileFilter Filter to apply to the documents in the source path for training.
* @param pollInterval Duration between each poll for the operation status. If none is specified, a default of
* 5 seconds is used.
*
* @return A {@link SyncPoller} that polls the extract receipt operation until it has completed, has failed,
* or has been cancelled. The completed operation returns a {@link CustomFormModel}.
* @throws HttpResponseException If training fails and model with {@link ModelStatus#INVALID} is created.
* @throws NullPointerException If {@code trainingFilesUrl} is {@code null}.
*/
@ServiceMethod(returns = ReturnType.SINGLE)
public SyncPoller<OperationResult, CustomFormModel> beginTraining(String trainingFilesUrl,
Expand All @@ -121,6 +127,7 @@ public SyncPoller<OperationResult, CustomFormModel> beginTraining(String trainin
* @param modelId The UUID string format model identifier.
*
* @return The detailed information for the specified model.
* @throws NullPointerException If {@code modelId} is {@code null}.
*/
@ServiceMethod(returns = ReturnType.SINGLE)
public CustomFormModel getCustomModel(String modelId) {
Expand All @@ -137,6 +144,7 @@ public CustomFormModel getCustomModel(String modelId) {
* @param context Additional context that is passed through the Http pipeline during the service call.
*
* @return The detailed information for the specified model.
* @throws NullPointerException If {@code modelId} is {@code null}.
*/
@ServiceMethod(returns = ReturnType.SINGLE)
public Response<CustomFormModel> getCustomModelWithResponse(String modelId, Context context) {
Expand Down Expand Up @@ -178,6 +186,7 @@ public Response<AccountProperties> getAccountPropertiesWithResponse(Context cont
* {@codesnippet com.azure.ai.formrecognizer.training.FormTrainingClient.deleteModel#string}
*
* @param modelId The UUID string format model identifier.
* @throws NullPointerException If {@code modelId} is {@code null}.
*/
@ServiceMethod(returns = ReturnType.SINGLE)
public void deleteModel(String modelId) {
Expand All @@ -194,6 +203,7 @@ public void deleteModel(String modelId) {
* @param context Additional context that is passed through the Http pipeline during the service call.
*
* @return A {@link Response} containing containing status code and HTTP headers
* @throws NullPointerException If {@code modelId} is {@code null}.
*/
@ServiceMethod(returns = ReturnType.SINGLE)
public Response<Void> deleteModelWithResponse(String modelId, Context context) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
package com.azure.ai.formrecognizer;

import com.azure.ai.formrecognizer.models.CustomFormModel;
import com.azure.ai.formrecognizer.models.ErrorInformation;
import com.azure.ai.formrecognizer.models.ErrorResponseException;
import com.azure.ai.formrecognizer.models.FormContentType;
import com.azure.ai.formrecognizer.models.FormPage;
Expand All @@ -13,6 +14,7 @@
import com.azure.ai.formrecognizer.training.FormTrainingAsyncClient;
import com.azure.ai.formrecognizer.training.FormTrainingClientBuilder;
import com.azure.core.credential.AzureKeyCredential;
import com.azure.core.exception.HttpResponseException;
import com.azure.core.http.HttpClient;
import com.azure.core.http.policy.HttpLogDetailLevel;
import com.azure.core.http.policy.HttpLogOptions;
Expand Down Expand Up @@ -44,7 +46,6 @@
import static org.junit.jupiter.api.Assertions.assertThrows;

public class FormRecognizerAsyncClientTest extends FormRecognizerClientTestBase {

private FormRecognizerAsyncClient client;

@BeforeAll
Expand Down Expand Up @@ -457,4 +458,22 @@ public void recognizeContentFromDataMultiPage(HttpClient httpClient, FormRecogni
});
}

@ParameterizedTest(name = DISPLAY_NAME_WITH_ARGUMENTS)
@MethodSource("com.azure.ai.formrecognizer.TestUtils#getTestParameters")
void recognizeCustomFormInvalidStatus(HttpClient httpClient, FormRecognizerServiceVersion serviceVersion) {
client = getFormRecognizerAsyncClient(httpClient, serviceVersion);
invalidSourceUrlRunner((invalidSourceUrl) -> beginTrainingLabeledRunner((training, useTrainingLabels) -> {
SyncPoller<OperationResult, CustomFormModel> syncPoller =
getFormTrainingAsyncClient(httpClient, serviceVersion).beginTraining(training, useTrainingLabels).getSyncPoller();
syncPoller.waitForCompletion();
CustomFormModel createdModel = syncPoller.getFinalResult();
HttpResponseException httpResponseException = assertThrows(HttpResponseException.class,
() -> client.beginRecognizeCustomFormsFromUrl(invalidSourceUrl, createdModel.getModelId()).getSyncPoller().getFinalResult());
ErrorInformation errorInformation = (ErrorInformation) ((List) httpResponseException.getValue()).get(0);
assertEquals("Analyze operation failed.", httpResponseException.getMessage());
assertEquals(EXPECTED_INVALID_URL_ERROR_CODE, errorInformation.getCode());
assertEquals(OCR_EXTRACTION_INVALID_URL_ERROR, errorInformation.getMessage());
}));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
package com.azure.ai.formrecognizer;

import com.azure.ai.formrecognizer.models.CustomFormModel;
import com.azure.ai.formrecognizer.models.ErrorInformation;
import com.azure.ai.formrecognizer.models.ErrorResponseException;
import com.azure.ai.formrecognizer.models.FormContentType;
import com.azure.ai.formrecognizer.models.FormPage;
Expand All @@ -13,6 +14,7 @@
import com.azure.ai.formrecognizer.training.FormTrainingClient;
import com.azure.ai.formrecognizer.training.FormTrainingClientBuilder;
import com.azure.core.credential.AzureKeyCredential;
import com.azure.core.exception.HttpResponseException;
import com.azure.core.http.HttpClient;
import com.azure.core.http.policy.HttpLogDetailLevel;
import com.azure.core.http.policy.HttpLogOptions;
Expand Down Expand Up @@ -350,6 +352,7 @@ public void recognizeContentFromDataMultiPage(HttpClient httpClient, FormRecogni
validateContentResultData(syncPoller.getFinalResult(), false);
});
}

@ParameterizedTest(name = DISPLAY_NAME_WITH_ARGUMENTS)
@MethodSource("com.azure.ai.formrecognizer.TestUtils#getTestParameters")
void recognizeCustomFormUrlMultiPageLabeled(HttpClient httpClient, FormRecognizerServiceVersion serviceVersion) {
Expand Down Expand Up @@ -419,4 +422,23 @@ public void recognizeContentFromUrlMultiPage(HttpClient httpClient, FormRecogniz
validateContentResultData(syncPoller.getFinalResult(), false);
});
}

@ParameterizedTest(name = DISPLAY_NAME_WITH_ARGUMENTS)
@MethodSource("com.azure.ai.formrecognizer.TestUtils#getTestParameters")
void recognizeCustomFormInvalidStatus(HttpClient httpClient, FormRecognizerServiceVersion serviceVersion) {
client = getFormRecognizerClient(httpClient, serviceVersion);
invalidSourceUrlRunner((invalidSourceUrl) -> {
beginTrainingLabeledRunner((training, useTrainingLabels) -> {
SyncPoller<OperationResult, CustomFormModel> syncPoller =
getFormTrainingClient(httpClient, serviceVersion).beginTraining(training, useTrainingLabels);
syncPoller.waitForCompletion();
CustomFormModel createdModel = syncPoller.getFinalResult();
HttpResponseException httpResponseException = assertThrows(HttpResponseException.class,
() -> client.beginRecognizeCustomFormsFromUrl(invalidSourceUrl, createdModel.getModelId()).getFinalResult());
ErrorInformation errorInformation = (ErrorInformation) ((List) httpResponseException.getValue()).get(0);
assertEquals(EXPECTED_INVALID_URL_ERROR_CODE, errorInformation.getCode());
assertEquals(OCR_EXTRACTION_INVALID_URL_ERROR, errorInformation.getMessage());
});
});
}
}
Loading

0 comments on commit f29e0da

Please sign in to comment.