NetSuite RESTlet Example (OAuth1): Send File Using Python

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.

Photo of author
Ryan Sheehy
Ryan has been dabbling in code since the late '90s when he cut his teeth exploring VBA in Excel. Having his eyes opened with the potential of automating repetitive tasks, he expanded to Python and then moved over to scripting languages such as HTML, CSS, Javascript and PHP.