Search code examples
pythongoogle-docsgoogle-docs-api

Styling Sections and Bulleted Lists via Google Docs API


I'm trying to automate some document creation in Google Docs via a Python script, and I think I haven't quite wrapped my head around how the Google Docs API's batchUpdate() method works.

I have my own JSON-based config file that my script reads in and generates some content in a corresponding Google Doc document. I'm essentially iterating over a dictionary of various sections destined for the document. My hope is to have a document structure like the following:

-- snip --

Section A

  • placeholder

Section B

  • placeholder

etc...

-- snip --

I have no issues creating some section headers (HEADING_2s), and I've been doing so in reverse order because it seems easier:

requests = []
curr_item = {}

for section in reversed(user_config["sections"]):
  # section text
  curr_item = {"insertText": {"location": {"index": 1}, "text": "{section_name}\n".format(section_name=i["name"])}}
  requests.append(curr_item)
  
  # section formatting (as an H2)
  curr_item = {"updateParagraphStyle": {"range": {"startIndex": 1, "endIndex": len(i["name"])}, "paragraphStyle": {"namedStyleType": "HEADING_2", "fields": "namedStyleType,spaceAbove,spaceBelow"}}
  requests.append(curr_item)

result = svc.documents().batchUpdate(documentId=doc_id, body={'requests': requests}).execute()

So far, so good 🙂

But adding placeholder bulleted lists into the mix has been causing me problems:

requests = []
curr_item = {}

bullet_placeholder = "placeholder"

for section in reversed(user_config["sections"]):
  # section text
  curr_item = {"insertText": {"location": {"index": 1}, "text": "{section_name}\n".format(section_name=i["name"])}}
  requests.append(curr_item)
  
  # section formatting (as an H2)
  curr_item = {"updateParagraphStyle": {"range": {"startIndex": 1, "endIndex": len(i["name"])}, "paragraphStyle": {"namedStyleType": "HEADING_2", "fields": "namedStyleType,spaceAbove,spaceBelow"}}
  requests.append(curr_item)

  # placeholder list
  curr_item = {"insertText": {"location": {"index": len(i["name"]) + 2}, "text": "placeholder"}, "createParagraphBullets": { "range": { "startIndex": len(i["name"]) + 2, "endIndex": len(bullet_placeholder) + 2 }, "bulletPreset": "BULLET_ARROW_DIAMOND_DISC" } }
  requests.append(curr_item)

result = svc.documents().batchUpdate(documentId=doc_id, body={'requests': requests}).execute()

This results in the following error response:

<HttpError 400 when requesting https://docs.googleapis.com/v1/documents/foo-document-id:batchUpdate?alt=json returned "Invalid value at 'requests[2]' (oneof), oneof field 'request' is already set. Cannot set 'createParagraphBullets'
Invalid value at 'requests[5]' (oneof), oneof field 'request' is already set. Cannot set 'createParagraphBullets'
Invalid value at 'requests[8]' (oneof), oneof field 'request' is already set. Cannot set 'createParagraphBullets'
Invalid value at 'requests[11]' (oneof), oneof field 'request' is already set. Cannot set 'createParagraphBullets'
Invalid value at 'requests[14]' (oneof), oneof field 'request' is already set. Cannot set 'createParagraphBullets'
Invalid value at 'requests[17]' (oneof), oneof field 'request' is already set. Cannot set 'createParagraphBullets'
Invalid value at 'requests[20]' (oneof), oneof field 'request' is already set. Cannot set 'createParagraphBullets'". Details: "[{'@type': 'type.googleapis.com/google.rpc.BadRequest', 'fieldViolations': [{'field': 'requests[2]', 'description': "Invalid value at 'requests[2]' (oneof), oneof field 'request' is already set. Cannot set 'createParagraphBullets'"}, {'field': 'requests[5]', 'description': "Invalid value at 'requests[5]' (oneof), oneof field 'request' is already set. Cannot set 'createParagraphBullets'"}, {'field': 'requests[8]', 'description': "Invalid value at 'requests[8]' (oneof), oneof field 'request' is already set. Cannot set 'createParagraphBullets'"}, {'field': 'requests[11]', 'description': "Invalid value at 'requests[11]' (oneof), oneof field 'request' is already set. Cannot set 'createParagraphBullets'"}, {'field': 'requests[14]', 'description': "Invalid value at 'requests[14]' (oneof), oneof field 'request' is already set. Cannot set 'createParagraphBullets'"}, {'field': 'requests[17]', 'description': "Invalid value at 'requests[17]' (oneof), oneof field 'request' is already set. Cannot set 'createParagraphBullets'"}, {'field': 'requests[20]', 'description': "Invalid value at 'requests[20]' (oneof), oneof field 'request' is already set. Cannot set 'createParagraphBullets'"}]}]">

I've tried a few other approaches, like keeping track of the index based on the length of all text written up to a given point in time, calling batch update at the end of each iteration of the for loop but all to no avail.

I'd be interested to know what I'm missing or misunderstanding about the APIs for inserting + formatting text and creating lists.


Solution

  • When I saw your showing request body from your script, I thought of the following modification points:

    Modification points:

    • In your script, for example, at curr_item = {"updateParagraphStyle": {"range": {"startIndex": 1, "endIndex": len(i["name"])}, "paragraphStyle": {"namedStyleType": "HEADING_2", "fields": "namedStyleType,spaceAbove,spaceBelow"}}, } is required to be added. So I'm worried that you might have miscopied your current script for replicating your issue.

    • About your script of So far, so good, I think that curr_item = {"updateParagraphStyle": {"range": {"startIndex": 1, "endIndex": len(i["name"])}, "paragraphStyle": {"namedStyleType": "HEADING_2", "fields": "namedStyleType,spaceAbove,spaceBelow"}} is required to be modified.

    • "fields": "namedStyleType,spaceAbove,spaceBelow", is moved to out side of paragraphStyle.

    • insertText and createParagraphBullets are required to be separated for each request.

    • If you want to achieve the following situation, I thought that the paragraph style of - placeholder is required to be also set.

      Section A

      • placeholder

      Section B

      • placeholder

      etc...

    When these points are reflected in your script, it becomes as follows. Unfortunately, from your showing script, I cannot understand the values of user_config and i. And, in your script, section is not used. So, I modified your showing script by using a sample values of ar = [{"section": "Section A", "placeholder": "placeholder"}, {"section": "Section B", "placeholder": "placeholder"}]. So, please modify this for your actual situation.

    Modified script:

    doc_id = "###" # Please set your Document ID.
    
    # This is a sample value.
    ar = [
        {"section": "Section A", "placeholder": "placeholder"},
        {"section": "Section B", "placeholder": "placeholder"},
    ]
    
    requests = []
    for e in reversed(ar):
        nextIndex = len(e["section"]) + 2
        requests = [
            *requests,
            {
                "insertText": {
                    "text": "{text}\n".format(text=e["section"]),
                    "location": {"index": 1},
                }
            },
            {
                "updateParagraphStyle": {
                    "range": {"startIndex": 1, "endIndex": 1},
                    "paragraphStyle": {"namedStyleType": "HEADING_2"},
                    "fields": "namedStyleType,spaceAbove,spaceBelow",
                }
            },
            {
                "insertText": {
                    "location": {"index": nextIndex},
                    "text": "{text}\n".format(text=e["placeholder"]),
                }
            },
            {
                "updateParagraphStyle": {
                    "range": {"startIndex": nextIndex, "endIndex": nextIndex},
                    "paragraphStyle": {"namedStyleType": "NORMAL_TEXT"},
                    "fields": "namedStyleType,spaceAbove,spaceBelow",
                }
            },
            {
                "createParagraphBullets": {
                    "range": {"startIndex": nextIndex, "endIndex": nextIndex},
                    "bulletPreset": "BULLET_ARROW_DIAMOND_DISC",
                }
            },
        ]
    result = svc.documents().batchUpdate(documentId=doc_id, body={"requests": requests}).execute()
    
    • About "namedStyleType": "HEADING_2", "fields": "namedStyleType,spaceAbove,spaceBelow", and "bulletPreset": "BULLET_ARROW_DIAMOND_DISC" are used from your showing script.

    Testing:

    When this script is run, the following result is obtained.

    enter image description here

    References: