How to update a page with Python using REST API?

Nick Hilton February 13, 2015

I would like to write content to an existing page using the REST API, here's my current script:

 

#!/usr/bin/python
#
# Reference: http://isbullsh.it/2012/06/Rest-api-in-python/
#

import sys
import getpass

import json
import requests

BASE_URL = "http://confluence.XXXXXX.com/rest/api/content"

PAGEID = 9470040

def main():

    username = raw_input('login: ')
    passwd = getpass.getpass()

    url = "{base}/{pageid}".format(
        base = BASE_URL, pageid = PAGEID)

    data = json.dumps(
    {
        'id' : '%d' % PAGEID,
        'type' : 'page',
        'title' : 'Sandbox',
        'space' : {'key' : 'HSWc'},
        'body' :
        {
            'storage' :
            {
                'representation' : 'storage',
                'value' : """
<table>
    <tbody>
        <tr>
            <th>Hello</th>
            <th>World</th>
        </tr>
        <tr>
            <td>Nick</td>
            <td>Rocks!</td>
        </tr>
    </tbody>
</table>
"""
            }
        }
    })

    r = requests.put(
        url,
        data = data,
        auth = (username, passwd),
        headers = {
            'Content-Type' : 'application/json',
            'Accept' : 'application/json'
            }
    )

    r.raise_for_status()


if __name__ == "__main__" : main()

But I'm getting "requests.exceptions.HTTPError: 400 Client Error: Bad Request"

There currently isn't any working documentation for using the REST API.  Please help!

5 answers

1 accepted

7 votes
Answer accepted
Nick Hilton February 20, 2015

I've now got a working example.  The documentation does not specify all the required fields to make this work, but it does state that the next version number must be specified.

So root cause to my failure was not explicitly specifying the next version number.

After I corrected that, the newly modified page was orphaned (https://jira.atlassian.com/browse/CRA-487).  But as sash pointed out, I found the same workaround.  Below is my complete working example:

$ python write_page.py -h
usage: write_page.py [-h] [-u USER] [-t TITLE] [-f FILE] pageid [html]
positional arguments:
  pageid                Specify the Conflunce page id to overwrite
  html                  Write the immediate html string to confluence page
optional arguments:
  -h, --help            show this help message and exit
  -u USER, --user USER  Specify the username to log into Confluence
  -t TITLE, --title TITLE
                        Specify a new title
  -f FILE, --file FILE  Write the contents of FILE to the confluence page
import argparse
import getpass
import sys

import json
import keyring
import requests

#-----------------------------------------------------------------------------
# Globals

BASE_URL = "http://confluence.XXXXX.com/rest/api/content"

VIEW_URL = "http://confluence.XXXXX.com/pages/viewpage.action?pageId="


def pprint(data):
    '''
    Pretty prints json data.
    '''
    print json.dumps(
        data,
        sort_keys = True,
        indent = 4,
        separators = (', ', ' : '))


def get_page_ancestors(auth, pageid):

    # Get basic page information plus the ancestors property

    url = '{base}/{pageid}?expand=ancestors'.format(
        base = BASE_URL,
        pageid = pageid)

    r = requests.get(url, auth = auth)

    r.raise_for_status()

    return r.json()['ancestors']


def get_page_info(auth, pageid):

    url = '{base}/{pageid}'.format(
        base = BASE_URL,
        pageid = pageid)

    r = requests.get(url, auth = auth)

    r.raise_for_status()

    return r.json()


def write_data(auth, html, pageid, title = None):

    info = get_page_info(auth, pageid)

    ver = int(info['version']['number']) + 1

    ancestors = get_page_ancestors(auth, pageid)

    anc = ancestors[-1]
    del anc['_links']
    del anc['_expandable']
    del anc['extensions']

    if title is not None:
        info['title'] = title

    data = {
        'id' : str(pageid),
        'type' : 'page',
        'title' : info['title'],
        'version' : {'number' : ver},
        'ancestors' : [anc],
        'body'  : {
            'storage' :
            {
                'representation' : 'storage',
                'value' : str(html),
            }
        }
    }

    data = json.dumps(data)

    url = '{base}/{pageid}'.format(base = BASE_URL, pageid = pageid)

    r = requests.put(
        url,
        data = data,
        auth = auth,
        headers = { 'Content-Type' : 'application/json' }
    )

    r.raise_for_status()

    print "Wrote '%s' version %d" % (info['title'], ver)
    print "URL: %s%d" % (VIEW_URL, pageid)


def get_login(username = None):
    '''
    Get the password for username out of the keyring.
    '''

    if username is None:
        username = getpass.getuser()

    passwd = keyring.get_password('confluence_script', username)

    if passwd is None:
        passwd = getpass.getpass()
        keyring.set_password('confluence_script', username, passwd)

    return (username, passwd)


def main():

    parser = argparse.ArgumentParser()

    parser.add_argument(
        "-u",
        "--user",
        default = getpass.getuser(),
        help = "Specify the username to log into Confluence")

    parser.add_argument(
        "-t",
        "--title",
        default = None,
        type = str,
        help = "Specify a new title")

    parser.add_argument(
        "-f",
        "--file",
        default = None,
        type = str,
        help = "Write the contents of FILE to the confluence page")

    parser.add_argument(
        "pageid",
        type = int,
        help = "Specify the Conflunce page id to overwrite")

    parser.add_argument(
        "html",
        type = str,
        default = None,
        nargs = '?',
        help = "Write the immediate html string to confluence page")

    options = parser.parse_args()

    auth = get_login(options.user)

    if options.html is not None and options.file is not None:
        raise RuntimeError(
            "Can't specify both a file and immediate html to write to page!")

    if options.html:
        html = options.html

    else:

        with open(options.file, 'r') as fd:
            html = fd.read()

    write_data(auth, html, options.pageid, options.title)


if __name__ == "__main__" : main()
absciexbuild March 10, 2017

Thanks for posting this, which saved me a great deal of time.  One thing to be careful of is if you are renaming the page by giving it a new title, then that title cannot be the same as the any of the titles in the space.  REST API will return a 400 error if there's a name clash.

Dirce Richards May 1, 2017

Hi Nick,

I am using your example. My code writes to the Confluence page only as text, I can't get Confluence to display it as a table. For example, I can write this:

 

<table><tbody><tr><th>a</th><th>b</th><th>c</th><th>d</th><th><p>e</p></th></tr><tr><td colspan="1">1</td><td colspan="1">2</td><td colspan="1">3</td><td colspan="1">4</td><td colspan="1">5</td></tr></tbody></table>

 

or I tried adding html elements to it, and it writes this:

<!DOCTYPE html> <html> <head> <title>Page Title</title> </head> <body> <h1>My First Heading</h1> <p>My first paragraph.</p> </body> </html>

If you are able to view this as a table, can you please tell me, what did you supply in the 'html' argument in your last example? 

Thanks,

Dirce

Dan McMahill March 6, 2018

Nick,

Thanks so much for posting this.  It is very helpful.  One thing I have found is that one html file may upload with no issues while a different html file throws a

400 Client Error:  Bad Request for url: ....

I think it may have to do with any errors in the html, but I'm not sure about that.  Have you run into this and do you have any solutions?

Thanks

-Dan

Alex Medved _ConfiForms_
Rising Star
Rising Star
Rising Stars are recognized for providing high-quality answers to other users. Rising Stars receive a certificate of achievement and are on the path to becoming Community Leaders.
March 6, 2018

@Dan McMahill

You could not post *any* HTML, it expects a storage format afaik

Dan McMahill March 7, 2018

I should have said storage format, not html.  But the problem is the same.  If the storage format file has any bug at all, it won't upload.  Maybe there is a way to validate the file locally?

Alex Medved _ConfiForms_
Rising Star
Rising Star
Rising Stars are recognized for providing high-quality answers to other users. Rising Stars receive a certificate of achievement and are on the path to becoming Community Leaders.
March 7, 2018
Keyan Kazemian July 10, 2018

@Dan McMahill I'm running into the same problem. Did you ever resolve this issue?

2 votes
Stan Ry
Rising Star
Rising Star
Rising Stars are recognized for providing high-quality answers to other users. Rising Stars receive a certificate of achievement and are on the path to becoming Community Leaders.
November 18, 2019

Following code worked for me.

I slightly modified the original code by getting rid of `auth` and changing it to session `s` variable. For some reason, I had to replace invocations o resquests module instances to HTTP session instances so that requests.get() and requests.post() becomes s.get() and s.post(). Otherwise I had been facing with SSL issues trying to run the code. My SSL connection is verified at proxy level, so SSL connection does become invalid. To ignore that I had to use:

requests.packages.urllib3.disable_warnings()

I am running it in Jupyter using a 64 bit Anaconda 3.

To run the code you have to:

1. Download Anaconda 3. Any will do, but 64-bit version is probably your best friend.

2. When installed, open Anaconda Navigator. It may take a whhhhhhiiiile before the Navigator starts especially if you are behind some proxy (it seems to fetch some external data which may be restricted on the proxy, like in my case).

3. From the Navigator window open Jupyter. It will start in your default browser.

4. In the started Jypyter tab (default address is http://localhost:8888/lab) create a new py file, rename it by right-clicking in the left navigation panel to whatever you like. Something like post_page.py will do.

5. Paste the code line by line into the notebook file.

6. Any Shift+Enter for the entered line executes the block of code. Any plain Enter just adds the edits to the specific code block.

You can execute the code line by line. I stripped off the _main_() call because I don't know how to call the main module from the notebook.

It seems wise to run this as architected by the author, but I failed to modify the code so that it runs without SSL issues from a PY file opened in JetBrains PyCharm.

I appreciate if someone fixes that the proper way.

Hope this little guide will help other rookies like me to get started with automated posting to Confluence.

Stan Ry
Rising Star
Rising Star
Rising Stars are recognized for providing high-quality answers to other users. Rising Stars receive a certificate of achievement and are on the path to becoming Community Leaders.
November 18, 2019
import requests
import json
import getpass

requests.packages.urllib3.disable_warnings() #ignoring HTTPS errors

auth = ('StanRy', getpass.getpass()) #put here your Confluence login instead of StanRy

s = requests.Session() #creating a single HTTP session
s.auth = auth
s.verify = False
s.headers = {"Content-Type": "application/json"}

BASE_URL = 'https://confluence.example.net/confluence_instance/rest/api/content'

def pprint(data):
'''
Pretty prints json data.
'''
print (json.dumps(
data,
sort_keys = True,
indent = 4,
separators = (', ', ' : '))
)

def get_page_ancestors(pageid):
# Get basic page information plus the ancestors property

url = '{base}/{pageid}?expand=ancestors'.format(
base = BASE_URL,
pageid = pageid)

r = s.get(url, auth = auth) #changed requests to 's' -- HTTP session object

r.raise_for_status()

return r.json()['ancestors']


def get_page_info(pageid):

url = '{base}/{pageid}'.format(
base = BASE_URL,
pageid = pageid)

r = s.get(url, auth = auth)

r.raise_for_status()

return r.json()


def write_data(html, pageid, title = None):

info = get_page_info(pageid)

ver = int(info['version']['number']) + 1

ancestors = get_page_ancestors(pageid)

anc = ancestors[-1]
del anc['_links']
del anc['_expandable']
del anc['extensions']

if title is not None:
info['title'] = title

data = {
'id' : str(pageid),
'type' : 'page',
'title' : info['title'],
'version' : {'number' : ver},
'ancestors' : [anc],
'body' : {
'storage' :
{
'representation' : 'storage',
'value' : str(html),
}
}
}

data = json.dumps(data)

url = '{base}/{pageid}'.format(base = BASE_URL, pageid = pageid)

r = s.put(
url,
data = data,
headers = { 'Content-Type' : 'application/json' }
)

r.raise_for_status()

print ("Wrote '%s' version %d" % (info['title'], ver))
print ("URL: %s%s" % (VIEW_URL, pageid)) #changed %d to % since I submit pageId as a string

#reading file with HTML data
with open(r'C:\\Users\\StanRy\\AppData\\Local\\Temp\\Test.txt') as htmlf:
htmld = htmlf.read()
print(htmld)

write_data(htmld, '1234567890') #pageId as a string

 

Stan Ry
Rising Star
Rising Star
Rising Stars are recognized for providing high-quality answers to other users. Rising Stars receive a certificate of achievement and are on the path to becoming Community Leaders.
November 18, 2019

Since this is a Python script, please check that all indentations are set correctly. For some reason, pasting Python code into the forum's form breaks all tabulations which are vital for Python.

Like Bruce Becker likes this
0 votes
Stan Ry
Rising Star
Rising Star
Rising Stars are recognized for providing high-quality answers to other users. Rising Stars receive a certificate of achievement and are on the path to becoming Community Leaders.
November 18, 2019

Hi, thanks for the script. Any clues on how to avoid SSL errors when running the script?

I am running this on a test server with non-trusted certificates, and want to get rid of SSL notifications like:

requests.exceptions.SSLError: HTTPSConnectionPool(host='confluence.example.net', port=443): Max retries exceeded with url: /confred/rest/api/content/1234567 (Caused by SSLError(SSLCertVerificationError(1, '[SSL: CERTIFICATE_VERIFY_FAILED] certificate
verify failed: unable to get local issuer certificate (_ssl.c:1051)')))

I have added 

requests.packages.urllib3.disable_warnings()

everywhere before calls of requests, but that didn't help.

Thank you.

0 votes
Alex Medved _ConfiForms_
Rising Star
Rising Star
Rising Stars are recognized for providing high-quality answers to other users. Rising Stars receive a certificate of achievement and are on the path to becoming Community Leaders.
February 20, 2015

Take a look at the discussions here https://answers.atlassian.com/questions/312039

It looks like it the issue is related to missing ancestors in your json

"ancestors":[  
      {  
         "id":"ID_OF_THE_PARENT_PAGE",
         "type":"page",
         "title":"TITLE OF THE PARENT PAGE"
      }],

Java example for quick ref https://bitbucket.org/jaysee00/confluence-rest-api-example/src/master/src/main/java/com/atlassian/api/examples/Main.java

0 votes
philleicht
Rising Star
Rising Star
Rising Stars are recognized for providing high-quality answers to other users. Rising Stars receive a certificate of achievement and are on the path to becoming Community Leaders.
February 16, 2015

Hey Nick,

did you happen to come across this documentation already?
Following the example curl-request to create a new page, you also have an example on how to update a page.

I hope this helps!

Regards,
Philipp


Nick Hilton February 16, 2015

That's the only example I could find on the planet. When I tried it, I got a strange error complaining about the header. I'm not at work today so I can't paste in the error message. But that's the example I'm trying to imitate with the script. It doesn't work.

Nick Hilton February 16, 2015

Also, those example are using strange expressions: &#39; which I think just means char ' So I think bash doesn't like seeing &#39;, I've tried just using ' instead but no joy.

philleicht
Rising Star
Rising Star
Rising Stars are recognized for providing high-quality answers to other users. Rising Stars receive a certificate of achievement and are on the path to becoming Community Leaders.
February 16, 2015

I see. Unfortunately I haven't messed with the API in a while and even then it was only JIRA and read-requests. Since the header is presented, it's strange that this would result in an error considering it matches the type of data provided. Yes, the &#39; are ' and are probably used for compatibility in case one decides to use single-quotes instead of quotes.

Suggest an answer

Log in or Sign up to answer
TAGS
AUG Leaders

Atlassian Community Events