Skip to content

Commit c2799de

Browse files
committed
feat: adding support for files
1 parent efd4196 commit c2799de

File tree

5 files changed

+286
-9
lines changed

5 files changed

+286
-9
lines changed

pom.xml

+5
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,11 @@
2828
<artifactId>jersey-media-json-jackson</artifactId>
2929
<version>2.31</version>
3030
</dependency>
31+
<dependency>
32+
<groupId>org.glassfish.jersey.media</groupId>
33+
<artifactId>jersey-media-multipart</artifactId>
34+
<version>2.30.1</version>
35+
</dependency>
3136
<dependency>
3237
<groupId>javax.xml.bind</groupId>
3338
<artifactId>jaxb-api</artifactId>

src/main/java/com/modzy/sdk/JobClient.java

+88-9
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
package com.modzy.sdk;
22

3-
import java.util.Arrays;
3+
import java.io.InputStream;
44
import java.util.List;
55
import java.util.Map;
6-
import java.util.Map.Entry;
76
import java.util.logging.Level;
87
import java.util.logging.Logger;
98

@@ -15,14 +14,23 @@
1514
import javax.ws.rs.client.WebTarget;
1615
import javax.ws.rs.core.GenericType;
1716
import javax.ws.rs.core.MediaType;
17+
import javax.ws.rs.core.Response;
18+
1819

19-
import com.fasterxml.jackson.core.JsonProcessingException;
2020
import com.fasterxml.jackson.core.type.TypeReference;
2121
import com.fasterxml.jackson.databind.ObjectMapper;
2222
import com.modzy.sdk.dto.JobHistorySearchParams;
2323
import com.modzy.sdk.exception.ApiException;
24-
import com.modzy.sdk.model.*;
24+
import com.modzy.sdk.model.Job;
25+
import com.modzy.sdk.model.JobInput;
26+
import com.modzy.sdk.model.JobInputStream;
27+
import com.modzy.sdk.model.JobStatus;
28+
import com.modzy.sdk.model.Model;
29+
import com.modzy.sdk.model.ModelVersion;
30+
import org.glassfish.jersey.media.multipart.MultiPart;
2531
import com.modzy.sdk.util.LoggerFactory;
32+
import org.glassfish.jersey.media.multipart.MultiPartFeature;
33+
import org.glassfish.jersey.media.multipart.file.StreamDataBodyPart;
2634

2735
/**
2836
*
@@ -100,23 +108,94 @@ public List<Job> getJobHistory(JobHistorySearchParams searchParams) throws ApiEx
100108
* @throws ApiException if there is something wrong with the service or the call
101109
*/
102110
public Job submitJob(Job job) throws ApiException{
111+
if( job.getInput() != null && job.getInput() instanceof JobInputStream){
112+
return submitOpenJob(job);
113+
}
103114
Builder builder = this.restTarget.request(MediaType.APPLICATION_JSON);
104115
builder.header("Authorization", "ApiKey "+this.apiKey);
105116
try {
106117
logger.info("creating job: "+job);
107118
job = builder.post(Entity.entity(job, MediaType.APPLICATION_JSON), Job.class);
108119
job.setStatus( JobStatus.SUBMITTED );
109120
return job;
110-
}
111-
catch(ResponseProcessingException rpe) {
121+
} catch(ResponseProcessingException rpe) {
112122
this.logger.log(Level.SEVERE, rpe.getMessage(), rpe);
113123
throw new ApiException(rpe);
124+
} catch(ProcessingException pr) {
125+
this.logger.log(Level.SEVERE, pr.getMessage(), pr);
126+
throw new ApiException(pr);
114127
}
115-
catch(ProcessingException pr) {
128+
}
129+
130+
/**
131+
* Call the Modzy API Services that post an open job and return it's final instance
132+
*
133+
* @param job
134+
* @return
135+
* @throws ApiException
136+
*/
137+
private Job submitOpenJob(Job job) throws ApiException{
138+
//Open the job
139+
Job openJob = this.submitJob(new Job(job.getModel()));
140+
//Iterate and submit the inputs
141+
try {
142+
JobInput<InputStream> jobInput = (JobInput<InputStream>) job.getInput();
143+
for (Map.Entry<String, Map<String, InputStream>> inputItem : jobInput.getSources().entrySet()) {
144+
for (Map.Entry<String, InputStream> dataItem : inputItem.getValue().entrySet()) {
145+
appendInput(openJob, inputItem.getKey(), dataItem.getKey(), dataItem.getValue());
146+
}
147+
}
148+
} catch( ApiException ae ){
149+
this.logger.log(Level.SEVERE, ae.getMessage(), ae);
150+
try{
151+
this.cancelJob(openJob);
152+
} catch( ApiException e2 ){
153+
this.logger.log(Level.WARNING, "Unpexpected exception trying to cancel the job", e2);
154+
}
155+
throw ae;
156+
}
157+
//Close the job
158+
return this.closeJob(openJob);
159+
}
160+
161+
private void appendInput(Job job, String inputItemKey, String dataItemKey, InputStream value) throws ApiException{
162+
Builder builder = this.restTarget
163+
.register(MultiPartFeature.class)
164+
.path(job.getJobIdentifier()).path(inputItemKey).path(dataItemKey)
165+
.request(MediaType.MULTIPART_FORM_DATA);
166+
builder.header("Authorization", "ApiKey "+this.apiKey);
167+
MultiPart data = new MultiPart();
168+
data.bodyPart( new StreamDataBodyPart("input", value, dataItemKey) );
169+
try {
170+
logger.info("Adding input: "+job.getJobIdentifier()+" "+inputItemKey+" "+dataItemKey);
171+
Response response = builder.post(Entity.entity(data, data.getMediaType()));
172+
if( response.getStatus() >= 400 ){
173+
throw new ApiException("The server respond with a status "+response.getStatus());
174+
}
175+
} catch(ResponseProcessingException rpe) {
176+
this.logger.log(Level.SEVERE, rpe.getMessage(), rpe);
177+
throw new ApiException(rpe);
178+
} catch(ProcessingException pr) {
179+
this.logger.log(Level.SEVERE, pr.getMessage(), pr);
180+
throw new ApiException(pr);
181+
}
182+
}
183+
184+
private Job closeJob(Job job) throws ApiException{
185+
Builder builder = this.restTarget.path(job.getJobIdentifier()).path("close").request(MediaType.APPLICATION_JSON);
186+
builder.header("Authorization", "ApiKey "+this.apiKey);
187+
try {
188+
logger.info("closing job: "+job);
189+
job = builder.post(Entity.entity(null, MediaType.APPLICATION_JSON), Job.class);
190+
job.setStatus( JobStatus.SUBMITTED );
191+
return job;
192+
} catch(ResponseProcessingException rpe) {
193+
this.logger.log(Level.SEVERE, rpe.getMessage(), rpe);
194+
throw new ApiException(rpe);
195+
} catch(ProcessingException pr) {
116196
this.logger.log(Level.SEVERE, pr.getMessage(), pr);
117197
throw new ApiException(pr);
118198
}
119-
120199
}
121200

122201
/**
@@ -129,7 +208,7 @@ public Job submitJob(Job job) throws ApiException{
129208
* @return the updated instance of the Job returned by Modzy API
130209
* @throws ApiException if there is something wrong with the service or the call
131210
*/
132-
public Job submitJob(Model model, ModelVersion modelVersion, JobInput<?> jobInput) throws ApiException{
211+
public Job submitJob(Model model, ModelVersion modelVersion, JobInput<?> jobInput) throws ApiException{
133212
return this.submitJob( new Job(model, modelVersion, jobInput) );
134213
}
135214

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package com.modzy.sdk.model;
2+
3+
import java.io.InputStream;
4+
5+
public class JobInputStream extends JobInput<InputStream>{
6+
7+
public JobInputStream() {
8+
super();
9+
}
10+
11+
public JobInputStream(ModelVersion modelVersion) {
12+
super(modelVersion);
13+
}
14+
15+
}

src/main/java/com/modzy/sdk/model/JobStatus.java

+1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.modzy.sdk.model;
22

33
public enum JobStatus {
4+
OPEN,
45
SUBMITTED,
56
IN_PROGRESS,
67
COMPLETED,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
package com.modzy.sdk.samples;
2+
3+
import com.fasterxml.jackson.databind.JsonNode;
4+
import com.modzy.sdk.ModzyClient;
5+
import com.modzy.sdk.dto.EmbeddedData;
6+
import com.modzy.sdk.exception.ApiException;
7+
import com.modzy.sdk.model.Job;
8+
import com.modzy.sdk.model.JobInput;
9+
import com.modzy.sdk.model.JobInputStream;
10+
import com.modzy.sdk.model.JobOutput;
11+
import com.modzy.sdk.model.JobStatus;
12+
import com.modzy.sdk.model.Model;
13+
import com.modzy.sdk.model.ModelInput;
14+
import com.modzy.sdk.model.ModelOutput;
15+
import com.modzy.sdk.model.ModelVersion;
16+
import io.github.cdimascio.dotenv.Dotenv;
17+
import org.apache.commons.io.IOUtils;
18+
19+
import java.io.ByteArrayInputStream;
20+
import java.io.File;
21+
import java.io.FileInputStream;
22+
import java.io.IOException;
23+
import java.io.InputStream;
24+
import java.util.HashMap;
25+
import java.util.Iterator;
26+
import java.util.Map;
27+
28+
public class JobFileInputSample {
29+
30+
public static void main(String[] args) throws ApiException, IOException {
31+
32+
// The system admin can provide the right base API URL, the API key can be downloaded from your profile page on Modzy.
33+
// You can config those params as is described in the readme file (as environment variables, or by using the .env file),
34+
// or you can just update the BASE_URL and API_KEY vars on this sample code (not recommended for production environments).
35+
36+
Dotenv dotenv = Dotenv.configure().ignoreIfMissing().load();
37+
38+
// The MODZY_BASE_URL should point to the API services route, it may be different from the Modzy page URL.
39+
// (ie: https://modzy.example.com/api).
40+
String baseURL = dotenv.get("MODZY_BASE_URL");
41+
// The MODZY_API_KEY is your own personal API key. It is composed by a public part, a dot character and a private part
42+
// (ie: AzQBJ3h4B1z60xNmhAJF.uQyQh8putLIRDi1nOldh).
43+
String apiKey = dotenv.get("MODZY_API_KEY");
44+
45+
// Client initialization
46+
// Initialize the ApiClient instance with the BASE_URL and the API_KEY to store those arguments
47+
// for the following API calls.
48+
ModzyClient modzyClient = new ModzyClient(baseURL,apiKey);
49+
50+
// Create a Job with a embedded input, wait, and retrieve results
51+
52+
// Get the model object:
53+
// If you already know the model identifier (i.e.: you get from the URL of the model details page or the input sample),
54+
// you can skip this step, if you don't you can find the model identifier by using its name as follows:
55+
Model model = modzyClient.getModelByName("Multi-Language OCR");
56+
// Or if you already know the model id and want to know more about the model, you can use this instead:
57+
// Model model = modzyClient.getModel("c60c8dbd79");
58+
// You can find more information about how to query the models on the ModelSamples.java file
59+
60+
// The model identifier is under the identifier property. You can take a look at the other properties under Model class
61+
// Or just log the model identifier, and potencially the latest version
62+
System.out.println(String.format("The model identifier is %s and the latest version is %s", model.getIdentifier(), model.getLatestVersion()));
63+
64+
// Get the model version object:
65+
// If you already know the model version and the input key(s) of the model version you can skip this step. Also, you can
66+
// use the following code block to know about the inputs keys, and skip the call on future job submissions.
67+
ModelVersion modelVersion = modzyClient.getModelVersion(model.getIdentifier(), model.getLatestVersion() );
68+
// The info stored in modelVersion provides insights about the amount of time that the model can spend processing,
69+
// the inputs, and output keys of the model.
70+
System.out.println(String.format("The model version is %s", modelVersion));
71+
System.out.println(String.format(" timeouts: status %sms, run %sms ",modelVersion.getTimeout().getStatus(), modelVersion.getTimeout().getRun()));
72+
System.out.println(" inputs:");
73+
for( ModelInput input : modelVersion.getInputs() ){
74+
System.out.println(
75+
String.format(" key %s, type %s, description: %s", input.getName(), input.getAcceptedMediaTypes(), input.getDescription())
76+
);
77+
}
78+
System.out.println(" outputs:");
79+
for( ModelOutput output : modelVersion.getOutputs() ){
80+
System.out.println(
81+
String.format(" key %s, type %s, description: %s", output.getName(), output.getMediaType(), output.getDescription())
82+
);
83+
}
84+
85+
// Send the job:
86+
// A file input will be submitted individually, so this method is best for large or multiple input files
87+
// You can use as a drop-in replacement instead of embedded inputs
88+
89+
//byte[] imageBytes = IOUtils.toByteArray( JobAwsInputSample.class.getClassLoader().getResourceAsStream("samples/image.png") );
90+
//byte[] configBytes = IOUtils.toByteArray( JobAwsInputSample.class.getClassLoader().getResourceAsStream("samples/config.json") );
91+
92+
String imagePath = "src/main/resources/samples/image.png";
93+
String configPath = "src/main/resources/samples/config.json";
94+
95+
// With the info about the model (identifier), the model version (version string, input/output keys), you are ready to
96+
// submit the job. Just prepare the source object:
97+
JobInput<InputStream> jobInput = new JobInputStream(modelVersion);
98+
Map<String,InputStream> mapSource = new HashMap<>();
99+
mapSource.put("input", new FileInputStream( imagePath ));
100+
mapSource.put("config.json", new FileInputStream( configPath ) );
101+
jobInput.addSource("source-key", mapSource);
102+
// An inference job groups input data that you send to a model. You can send any amount of inputs to
103+
// process and you can identify and refer to a specific input by the key that you assign, for example we can add:
104+
mapSource = new HashMap<>();
105+
mapSource.put("input", new FileInputStream( imagePath ) );
106+
mapSource.put("config.json", new FileInputStream( configPath ) );
107+
jobInput.addSource("second-key", mapSource);
108+
// You don't need to load all the inputs from files, just convert from bytes as follows:
109+
byte[] configBytes = "{\"languages\": [\"spa\"]}".getBytes();
110+
mapSource = new HashMap<>();
111+
mapSource.put("input", new FileInputStream( imagePath ) );
112+
mapSource.put("config.json", new ByteArrayInputStream(configBytes));
113+
jobInput.addSource("another-key", mapSource);
114+
//If you send a wrong input key, the model fails to process the input.
115+
mapSource = new HashMap<>();
116+
mapSource.put("a.wrong.key", new FileInputStream( imagePath ));
117+
mapSource.put("config.json", new FileInputStream( configPath ));
118+
jobInput.addSource("wrong-key", mapSource);
119+
// If you send a correct input key, but some wrong values, the model fails to process the input.
120+
mapSource = new HashMap<>();
121+
mapSource.put("input", new FileInputStream( configPath ));
122+
mapSource.put("config.json", new FileInputStream( imagePath ));
123+
jobInput.addSource("wrong-values", mapSource);
124+
125+
// When you have all your inputs ready, you can use our helper method to submit the job as follows:
126+
Job job = modzyClient.submitJob(model, modelVersion, jobInput);
127+
// Modzy creates the job and queue for processing. The job object contains all the info that you need to keep track
128+
// of the process, the most important being the job_identifier and the job status.
129+
System.out.println(String.format("job: %s", job));
130+
// The job moves to SUBMITTED, meaning that Modzy acknowledged the job and sent it to the queue to be processed.
131+
// We provide a helper method to hold until the job finishes processing. Its a good practice to set a max timeout
132+
// if you're doing a test (ie: 2*status+run). in any case, it will hold until the job
133+
// finishes and moves to COMPLETED, CANCELED, or TIMEOUT.
134+
job = modzyClient.blockUntilComplete(job, null);
135+
136+
// Get the results:
137+
// Check the status of the job. Jobs may be canceled or can reach a timeout.
138+
if( JobStatus.COMPLETED.equals( job.getStatus() ) ){
139+
// A completed job means that all the inputs were processed by the model. Check the results for each
140+
// input key provided in the source map to see the model output.
141+
JobOutput<JsonNode> result = modzyClient.getResult(job);
142+
// The result object has some useful info:
143+
System.out.println(
144+
String.format("Result: finished: %s, total: %s, completed: %s, failed: %s",
145+
result.getFinished(), result.getTotal(), result.getCompleted(), result.getFailed()
146+
)
147+
);
148+
// Notice that we are iterating thought the same keys of the input sources
149+
for( String key : jobInput.getSources().keySet() ){
150+
// The result object has the individual results of each job input. In this case the output key is called
151+
// results.json, so we can get that specific model result as follows:
152+
if( result.getResults().containsKey(key) ){
153+
JsonNode modelResult = result.getResults().get(key).get("results.json");
154+
// The output for this model comes in a JSON format, so we can directly log the model results:
155+
System.out.print(String.format(" %s: ", key));
156+
Map.Entry<String, JsonNode> field;
157+
String textValue;
158+
for (Iterator<Map.Entry<String, JsonNode>> it = modelResult.fields(); it.hasNext(); ) {
159+
field = it.next();
160+
textValue = field.getValue().asText().replace('\n', ' ');
161+
System.out.print( String.format(" %s: %s", field.getKey(), textValue) );
162+
}
163+
System.out.println();
164+
}
165+
else{
166+
// If the model raises an error, we can get the specific error message:
167+
System.err.println(String.format(" %s: failure: %s", key, result.getFailures().get(key)));
168+
}
169+
}
170+
}
171+
else{
172+
System.err.println(String.format("processing failed: %s", job));
173+
}
174+
175+
}
176+
177+
}

0 commit comments

Comments
 (0)