How do you send a file through to your NetSuite instance via a RESTlet using a Python Script?
If you know how to
send a
POST
request through to a RESTlet using Python
then only minor modifications need to be made to enable the sending of a file.
One reason why you may prefer sending a file for NetSuite process is where you have a lot of data to consume and would prefer sending the consumption of the data through a Map/Reduce script. The RESTlet would then be responsible for capturing the data files, and then triggering the Map/Reduce script to process the data.
The response from the RESTlet back to the end-user would then only be the storage ID of the file.
You could further extend your script to notify someone of the success of the file processing.
In this example, I’ll explore the basic structure of the RESTlet file and show you how I went about processing this with a Map/Reduce script and how an email was sent when the file had finished processing.
Create RESTlet Script
Try to keep your RESTlet as simple as possible so that it helps you to know whether you are having connection issues, or authentication issues, or something just relating to getting the data out of or into NetSuite.
By bloating your RESTlet with too much code it can make it difficult to diagnose where the issue might lie.
Therefore, with my RESTlet where I’m capturing data from the end-user I want to log the input received to make sure it is matching the configuration of the request sent, and then I want to perform all the necessary checks and validations before passing the RESTlet with its data to a new process.
Here’s an example of how my RESTlet has started:
/**
* @NApiVersion 2.1
* @NScriptType Restlet
*/
define(['N/file', 'N/error', 'N/format'],
/**
* @params {file} file
* @params {error} error
* @params {format} format
*/
(file, error, format) => {
const doValidation = (props, propNames) => {
const errors = props.reduce(
(accum, prop, idx) => {
if (!prop && prop !== 0) accum.push(`Missing a required argument: ${propNames[idx]}`);
return accum;
}
, []
);
if (errors.length) throw error.create({ name: 'MISSING_REQUIRED_ARGS', message: errors.join("\n") });
}
const post = (params) => {
log.debug({title: 'POST request (params)', details: params});
doValidation([params.contents, params.fileName, params.folderId, params.fileType, params.emailUser], ['contents', 'fileName', 'folderId', 'fileType', 'emailUser']);
// perform upload of file to File Cabinet
const fileRecord = file.create({ name: params.fileName, fileType: params.fileType, contents: params.contents,
folder: params.folderId, encoding: file.Encoding.UTF_8, isOnline: false });
const fileId = fileRecord.save();
return fileId;
}
return {post}
});
As you can see from the above code there are more properties you want to check with a file request being issued through the RESTlet, namely,
contents
,
fileName
,
folderId
,
fileType
and a non-required field for the
N/file
module, but one needed to alert the user of the finished Map/Reduce script
emailUser
. The other properties are needed when creating a
file
in NetSuite and you will therefore want to check these have been issued through.
Also with this simple example I’ve included the code of uploading the file within the RESTlet script itself, I generally wouldn’t do this and would rather have the code as a separate module, but the point being illustrated here is how uploading the file into the File Cabinet from a RESTlet works.
One other quick aside while I’m here: you may need to add/edit the role
Documents and Files
in the
List
area to at least the
Create
level for the RESTlet user to be able to store the file in your File Cabinet.
Trigger Map/Reduce Script
The second component to the script is the triggering of the Map/Reduce Script once the file has been successfully loaded into the File Cabinet.
As there are other aspects to do after this script has run I’m going to create parameters on the Map/Reduce script so that the RESTlet can send through these property values and the Map/Reduce script when triggered will read these values and process away.
Therefore, the additional code in the RESTlet script will now add the following
before
the
return fileId;
line:
const fileId = fileAP.save();
// ... INSERT CODE BELOW ...
// trigger the Map/Reduce script
const taskMapReduce = task.create({
taskType: task.TaskType.MAP_REDUCE,
scriptId: "customscriptX",
deployId: "customdeployX",
params: {
'custscript_mr_param_fileid': fileId,
'custscript_mr_param_email': requestBody.emailAlert
}
});
// in case there are issues triggering the Map/Reduce script:
try {
const taskMapReduceId = taskMapReduce.submit();
let taskMapReduceStatus = task.checkStatus({taskId: taskMapReduceId});
if (taskMapReduceStatus !== task.TaskStatus.FAILED) {
log.debug({title: 'Deployment Success', details: taskMapReduceStatus});
}
} catch(e) {
throw error.create({title: 'Map/Reduce Script Failed', details: e});
}
// ... INSERT CODE ABOVE ...
return fileId;
Then the corresponding Map/Reduce script will look something like this:
/**
* @NApiVersion 2.1
* @NScriptType MapReduceScript
*/
define(['N/runtime', 'N/file', 'N/format', 'N/email', './create_record'],
/**
* @param {runtime} runtime
* @param {file} file
* @param {format} format
* @param {email} email
* @param {FileCreateStuff} createStuff
*/
(runtime, file, format, email, createStuff) => {
const getInputData = (inputContext) => {
log.debug({title: 'M/R Started', details: inputContext});
const cs = runtime.getCurrentScript();
const fileId = cs.getParameter({name: 'custscript_mr_param_fileid'});
// as the file is JSON, parsing needed
return JSON.parse(file.load({id: fileId}).getContents());
}
const map = (mapContext) => {
log.debug({title: 'MAP (params)', details: mapContext});
const val = JSON.parse(mapContext.value);
// with the values do any modifications and then insert into your record insert/update module
createStuff(...);
}
const reduce = (reduceContext) => {
// only use if needed
}
const summarize = (summaryContext) => {
const cs = runtime.getCurrentScript();
const fileId = cs.getParameter({name: 'custscript_mr_param_fileid'});
// delete file
file.delete({id: fileId});
const recipients = cs.getParameter({name: 'custscript_mr_param_email'});
const body = `This email is to notify you that the processing of XYZ has now been completed.\n
This process used ${summaryContext.usage} units.\nAlso, the file imported ${fileId} has also been removed from the File Cabinet.`;
email.send({
author: 0, // NetSuite ID
body,
recipients,
subject: `Import of XYZ`
});
}
return {getInputData, map, summarize}
});
As you can see from the Map/Reduce script above you can obtain the
fileId
and load the contents of the file into the script. As I sent through a JSON file I needed to
JSON.parse
the contents to convert it to a useable format.
(Be aware when sending a JSON file the limit of file sizes is 10MB. JSON files can be quite large and you might need to look at either breaking up your JSON files into smaller bits to digest, or using a different file type such as CSV. With a recent project where I sent the data as JSON the size of the file imported into the File Cabinet was 4.5MB whereas when I translated it to just CSV the file size was 1.1MB.)
Once the data is then issued to create the necessary records the final step is seen in the
summarize
function where the file uploaded is removed, and an email is sent to the user who was inserted into the user from the initial
POST
request in Python.
I tend to use the user who created the Access Key and Token as the author, but you can use whatever employee record you want.
One final aside with the functionality of triggering a script to run from the RESTlet: you may need to add/edit the role
Suitescript Scheduling
in the
Setup
tab to enable the REST user to invoke the Map/Reduce script.
Python Script
The final element is the Python script which is no different to what I had previously except the need for more required parameters in the
data
property:
from requests_oauthlib import OAuth1Session
import json
CLIENT_KEY: str = "HASH"
CLIENT_SECRET: str = "HASH"
ACCESS_KEY: str = "HASH"
ACCESS_SECRET: str = "HASH"
SIGNATURE_METHOD: str = "HMAC-SHA256"
REALM_ID: str = "1234567"
SCRIPT_ID: int = 1
DEPLOY_ID: int = 1
URL: str = f"https://{REALM_ID}.restlets.api.netsuite.com/app/site/hosting/restlet.nl?script={SCRIPT_ID}&deploy={DEPLOY_ID}
oauth = OAuth1Session(
client_key=CLIENT_KEY,
client_secret=CLIENT_SECRET,
resource_owner_key=ACCESS_KEY,
resource_owner_secret=ACCESS_SECRET,
realm=REALM_ID
signature_method=SIGNATURE_METHOD
)
data = {"emailUser" : "test@example.com", "fileName": "ABC.JSON", "fileType": "JSON", "folderId": 123, "contents": {"A": 1, "B": 2, "C": 3}}
headers = {
"Content-Type": "application/json"
}
res = oauth.post(URL, data=json.dumps(data), headers=headers)
print(res)
The same script as before, just more data inserted into the
data
variable that is sent through to the RESTlet.
When you trigger the Python script it will then send through the data collated in the
contents
property of the
dictionary
and will store it as a
JSON
file in the File Cabinet inside the
folderId
.
Due to the nature of the RESTlet it will then store the data, and then send back a
200
response with the fileId as the
text
of that response. In the meantime, the Map/Reduce is triggered to perform the work of creating the records needed according to the uploaded JSON file submitted.
When this is all done, you will then receive an email alert notifying you of the result.
Summary
Sending a file through a RESTlet can easily be achieved provided the contents of the file can be transmitted. Sending a file rather than multiple individual requests can help process large quantities of data in your NetSuite instance.