pythongoogle-apps-scriptgoogle-apps-script-api

Calling Google Apps Script functions with Python: "Requested entity was not found"


Trying to call a GAS function with Python from Cloud Shell Editor, but not sure what I'm doing wrong. I scoured through stackoverflow, and tried using executable API, but that forced me to use OAuth2.0 which I could never get it to work (tells me to authorize through a link then it 403's me). The function below at least lists the functions that I have on GAS, but the trying to run the function returns a 404.

from __future__ import print_function
# bot.py
import os
import pickle
import os.path
import google.auth
from google.oauth2 import service_account
from googleapiclient.discovery import build

# Replace 'your-service-account.json' with the path to your service account JSON file
service_account_json = 'service.json'

# Replace with your Google Apps Script project ID
script_id = 'UNIQUE-SCRIPTID'

# Define the scopes required for the Google Apps Script API
SCOPES = [
    'https://www.googleapis.com/auth/script.projects',
    'https://www.googleapis.com/auth/script.scriptapp'
    ]


def authorize_script_api(service_account_json):
    # Load the service account credentials from the JSON file
    creds = google.oauth2.service_account.Credentials.from_service_account_file(
        service_account_json, scopes=SCOPES)
    
    return creds

def list_functions(script_id, credentials):
    try:
        # Build the Google Apps Script API service
        service = build('script', 'v1', credentials=credentials)
        
        # Get the content of the specified Google Apps Script project
        content = service.projects().getContent(scriptId=script_id).execute()
        
        if 'files' in content:
            for file in content['files']:
                if 'functionSet' in file:
                    function_set = file['functionSet']
                    if 'values' in function_set:
                        for func in function_set['values']:
                            if 'name' in func:
                                print(func['name'])
    except Exception as e:
        print(f"An error occurred: {e}")

def run_function(script_id, credentials, function_name):
    try:
        # Build the Google Apps Script API service
        service = build('script', 'v1', credentials=credentials)
        
        # Create a request to run the specified function
        request = {
            "function": function_name
        }

        # Execute the function in the Google Apps Script project
        response = service.scripts().run(scriptId=script_id, body=request).execute()

        print(response)
    except Exception as e:
        print(f"An error occurred: {e}")

if __name__ == '__main__':
    # Authorize the script API
    credentials = authorize_script_api(service_account_json)
    
    # List functions in the Google Apps Script project
    list_functions(script_id, credentials)

    # Specify the name of the function you want to run
    function_name = "helloworld"
    
    # Run the specified function in the Google Apps Script project
    run_function(script_id, credentials, function_name)

returns the following:

helloworld
setTrigger
scheduledTrigger
deleteTriggers
An error occurred: <HttpError 404 when requesting https://script.googleapis.com/v1/scripts/UNIQUE-SCRIPTID:run?alt=json returned "Requested entity was not found.". Details: "Requested entity was not found.">

This is what content returns:

{'scriptId': 'UNIQUE-SCRIPTID', 'files': [{'name': 'appsscript', 'type': 'JSON', 'source': '{
  "timeZone": "America/Los_Angeles",
  "dependencies": {},
  "exceptionLogging": "STACKDRIVER",
  "runtimeVersion": "V8",
  "executionApi": {
    "access": "MYSELF"
  },
  "oauthScopes": [
    "https://www.googleapis.com/auth/drive.readonly",
    "https://www.googleapis.com/auth/drive",
    "https://www.googleapis.com/auth/spreadsheets",
    "https://www.googleapis.com/auth/script.external_request",
    "https://www.googleapis.com/auth/script.scriptapp"
  ]
}', 'lastModifyUser': {'photoUrl': 'redacted'}, 'createTime': '2022-09-06T01:19:20.404Z', 'updateTime': '2023-10-09T07:08:55.871Z', 'functionSet': {}}, {'name': 'Code', 'type': 'SERVER_JS', 'source': 'function helloworld() {
  console.log("hello!");
}

function setTrigger() {
  deleteTriggers();  
  var times = [[19,15]]; // 7:15PM
  times.forEach(t_el => scheduledTrigger(t_el[0],t_el[1]));
}

function scheduledTrigger(hours,minutes) {
  var today_D = new Date();  
  var year = today_D.getFullYear();
  var month = today_D.getMonth();
  var day = today_D.getDate();
    
  pars = [year,month,day,hours,minutes];
    
  var scheduled_D = new Date(...pars);
  var hours_remain=Math.abs(scheduled_D - today_D) / 36e5;
  ScriptApp.newTrigger("Announcement")
  .timeBased()
  .after(hours_remain * 60 *60 * 1000)
  .create()
}

function deleteTriggers() {
  var triggers = ScriptApp.getProjectTriggers();
  for (var i = 0; i < triggers.length; i++) {
    if (   triggers[i].getHandlerFunction() == "Announcement") {
      ScriptApp.deleteTrigger(triggers[i]);
    }
  }
}
}', 'lastModifyUser': {'photoUrl': 'redacted'}, 'createTime': '2022-09-06T01:19:20.397Z', 'updateTime': '2023-10-09T07:08:55.871Z', 'functionSet': {'values': [{'name': 'helloworld'}, {'name': 'setTrigger'}, {'name': 'scheduledTrigger', 'parameters': ['hours', 'minutes']}, {'name': 'deleteTriggers'}]}}]}

Solution

  • Issue and workaround

    When I saw your showing script, I was worried that the reason for your current issue might be due to Warning: The Apps Script API does not work with service accounts.. Ref And, when I tested your situation using the service account, in the current stage, it seems that the script can be retrieved using the service account. However, it seems that the script cannot be run using the service account, and I confirmed the same error message with you. I'm worried that this might be the reason for your current issue.

    In order to achieve your goal, in that case, as a workaround, how about executing your Google Apps Script using Web Apps? I think that when Web Apps is used, the Google Apps Script can be run with the service account. The flow of this workaround is as follows.

    1. Share your Google Apps Script with a service account

    Please share your Google Apps Script with the email of the service account. If you have already done this, it is not required to run this.

    2. Modify Google Apps Script

    Please add the following function to your Google Apps Script.

    function doGet(e) {
      const { functionName } = e.parameter;
      return ContentService.createTextOutput((functionName && this[functionName]) ? this[functionName]() : "No function.");
    }
    

    3. Deploy Web Apps

    The detailed information can be seen in the official document.

    Please set this using the new IDE of the script editor.

    1. On the script editor, at the top right of the script editor, please click "click Deploy" -> "New deployment".
    2. Please click "Select type" -> "Web App".
    3. Please input the information about the Web App in the fields under "Deployment configuration".
    4. Please select "Me" for "Execute as".
    5. Please select "Anyone with Google account" for "Who has access to the app:".
      • By this, the service account can access Web Apps with the access token.
    6. Please click "Deploy" button.
    7. When "The Web App requires you to authorize access to your data." is shown, please click "Authorize access" button. And, please authorize the scopes.
    8. Copy the URL of the Web App. It's like https://script.google.com/macros/s/###/exec. This URL is used with Python script.

    4. Modify Python script

    Please set your Web Apps URL to url = "https://script.google.com/macros/s/###/exec" # Please set your Web Apps URL..

    from __future__ import print_function
    
    import requests
    import google.auth
    from google.oauth2 import service_account
    from googleapiclient.discovery import build
    
    # Replace 'your-service-account.json' with the path to your service account JSON file
    service_account_json = 'service.json'
    
    # Replace with your Google Apps Script project ID
    script_id = 'UNIQUE-SCRIPTID'
    
    # Define the scopes required for the Google Apps Script API
    SCOPES = [
        "https://www.googleapis.com/auth/script.projects",
        "https://www.googleapis.com/auth/script.scriptapp",
        "https://www.googleapis.com/auth/drive.readonly" # Added. This is used for requesting Web Apps.
    ]
    
    
    def authorize_script_api(service_account_json):
        creds = google.oauth2.service_account.Credentials.from_service_account_file(service_account_json, scopes=SCOPES)
        return creds
    
    
    def list_functions(script_id, credentials):
        try:
            service = build("script", "v1", credentials=credentials)
            content = service.projects().getContent(scriptId=script_id).execute()
            if "files" in content:
                for file in content["files"]:
                    if "functionSet" in file:
                        function_set = file["functionSet"]
                        if "values" in function_set:
                            for func in function_set["values"]:
                                if "name" in func:
                                    print(func["name"])
        except Exception as e:
            print(f"An error occurred: {e}")
    
    
    def run_function(credentials, function_name):
        try:
            url = "https://script.google.com/macros/s/###/exec" # Please set your Web Apps URL.
    
            access_token = credentials.token
            url += "?functionName=" + function_name
            res = requests.get(url, headers={"Authorization": "Bearer " + access_token})
            print(res.text)
    
        except Exception as e:
            print(f"An error occurred: {e}")
    
    
    if __name__ == "__main__":
        # Authorize the script API
        credentials = authorize_script_api(service_account_json)
    
        # List functions in the Google Apps Script project
        list_functions(script_id, credentials)
    
        # Specify the name of the function you want to run
        function_name = "helloworld"
    
        # Run the specified function in the Google Apps Script project
        run_function(credentials, function_name)
    

    Testing:

    When I tested this modified script, I could retrieve the function names from list_functions. And also, I could retrieve the response value from run_function.

    Note:

    References: