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!
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()
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.
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
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
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
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
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
You could not post *any* HTML, it expects a storage format afaik
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
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?
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
May be you can try to convert it: https://developer.atlassian.com/cloud/confluence/rest/#api-contentbody-convert-to-post
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
@Dan McMahill I'm running into the same problem. Did you ever resolve this issue?
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
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.
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
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
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
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.
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
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.
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
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
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
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
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
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.
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
Also, those example are using strange expressions: ' which I think just means char ' So I think bash doesn't like seeing ', I've tried just using ' instead but no joy.
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
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 ' are ' and are probably used for compatibility in case one decides to use single-quotes instead of quotes.
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.
You must be a registered user to add a comment. If you've already registered, sign in. Otherwise, register and sign in.