Sample CouchDB curl session

CouchDB has a lot of documentation in its wiki. A good overview of how to access it directly through HTTP is at this wiki page. When starting learning CouchDB I found it quite succinct. This is great after you are “in business” and just need to confirm something or refresh your memory, but I think it’s hard for the first couple of sessions.

The below sample is a sample interaction that shows:

  • Using pure curl access – the basics
  • The following operations:
    • Creating a database
    • Deleting a database
    • Creating documents
    • Creating design documents
    • Which implies updating documents, as design documents are nothing but regular documents with a special name
    • Creating views
    • Updating views
    • Getting view results
  • Python curl is used for simplicity – this could have been done through command line curl or using some other programming language
  • Actual HTTP calls if enabled via
    • conn.set_debuglevel(1) setting (see code)
  • Most importantly – how much easier is to use some of the libraries (such as these) that wrap all this in simpler calls, after you grasp the basics

Some important notes:

  • Be sure to change the parameters at the start of the script if they are different
  • Important: this is destructive – it will ruin the given database, so be careful
  • For the above reason, I put the ‘raise Exception’ statement – comment it out to run the code

Here’s the code:

#!/usr/bin/python

# License: MIT (http://www.opensource.org/licenses/mit-license.php)
#
# Copyright (c) 2011. icyrock.com
# 
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation files
# (the "Software"), to deal in the Software without restriction,
# including without limitation the rights to use, copy, modify, merge,
# publish, distribute, sublicense, and/or sell copies of the Software,
# and to permit persons to whom the Software is furnished to do so,
# subject to the following conditions:
# 
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
# 
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

# --------------------------------------------
# Sample plain HTTP Python <-> CouchDB session
# --------------------------------------------
#
# This sample python script is a small curl-based interaction with
# CouchDB. When you run it, you should be able to see what you can
# actually do using low-level curl interface that CouchDB supports. If
# you uncomment the following line:
#
# #conn.set_debuglevel(1)
#
# you will see pure HTTP calls that are actually being made. Looking at
# these you should get a better understanding about how CouchDB works
# if you take out all the libraries that you might use and which cover
# the details of what actually happens beneath.
#
# Of course, after you have this all figured out, go and use the
# library - it's easier that way. Take a look at this page for more
# info: http://wiki.apache.org/couchdb/Getting_started_with_Python

import httplib
import urllib2
import json
from textwrap import dedent

# Some defaults, change to match your system
# !!! Note the database dbname will be destroyed !!!
base_url = 'localhost:5984'
dbname = 'robocouch'
docprefix = 'rcd'
raise Exception('Comment out after confirming the above parameters')

# Some helpers
class PropertyDict(dict):
  def __init__(self, **items):
    self.__dict__.update(items)
    for k in items:
      self[k] = items[k]

conn = httplib.HTTPConnection(base_url)
# If you want to see the actual HTTP calls made by httplib, uncomment the below
# Very useful to grasp the low level stuff
#conn.set_debuglevel(1)

def couchdb_http_action(method, url, body=None):
  if body != None:
    json_send = json.dumps(body)
    headers = {'Content-Type': 'application/json'}
    conn.request(method, '/%s' % url, json_send, headers)
  else:
    conn.request(method, '/%s' % url)
  resp = conn.getresponse()
  raw = resp.read()
  info = PropertyDict(
    status=resp.status,
    raw=raw,
    data=json.loads(raw),
  )
  return info

# Helper methods - these basically do a HTTP call as given the name 
# For example, hget = HTTP GET
def hget(url):
  return couchdb_http_action('GET', url)

def hpost(url, data):
  return couchdb_http_action('POST', url, data)

def hdelete(url):
  return couchdb_http_action('DELETE', url)

def hput(url, data=None):
  return couchdb_http_action('PUT', url, data)

def pretty(data):
#  return json.dumps(data, sort_keys=True, indent=2)
  return json.dumps(data, indent=2)

#
# Let's get to work 
#
print('Welcome!')

# Create our database, delete if currently existing
print('Our database: %s' % dbname)
alldbs = hget('_all_dbs').data
print('All databases: %s' % alldbs)

if dbname in alldbs:
  print('Our database %s is present, deleting' % dbname)
  hdelete(dbname)
  print('Deleted %s, all databases: %s' % (dbname, hget('_all_dbs').data))

hput(dbname)
print('Created database %s, all databases: %s' % (dbname, hget('_all_dbs').data))

# Create some documents
for i in range(1, 6):
  docid = '%s%d' % (docprefix, i) 
  print('Creating document %s' % docid)
  hput('%s/%s' % (dbname, docid), dict(number=i*33))
#print('All document IDs in %s: %s' % (dbname, [row['id'] for row in hget('%s/_all_docs' %dbname).data['rows']]))
print('All document IDs in %s: %s' % (dbname, hget('%s/_all_docs' %dbname).raw))

# Create a design document with the same name as our database (defined by dbname variable)
# It will contain a single view called 'all_numbers'
# This view will have only a map part and will return all documents with 'number' attribute
hput('%s/_design/%s' % (dbname, dbname), dict(
    language='javascript',
    views=dict(
      all_numbers=dict(
        map=dedent('''
               function(doc) {
                 if(doc.number) {
                   emit(null, doc)
                 }
               }
            ''')
      ),
    )
))

# Actually run the view
print("All documents with 'number' attribute:")
#print(pretty(hget('%s/_design/%s/_view/all_numbers' % (dbname, dbname)).data))
print(hget('%s/_design/%s/_view/all_numbers' % (dbname, dbname)).raw)
 
# Update the design document by adding another view called count_sum_numbers
# Since this is an update, we need to fetch to get the current revision of the document
# The revision is part of our JSON outbound request
# This one will sum all documents which have 'number' attribute and give us their count
# Note that the design document is a whole, thus our previous view needs to be preserved
rev = hget('%s/_design/%s' % (dbname, dbname)).data['_rev']
print('Updating revision %s' % rev)
hput('%s/_design/%s' % (dbname, dbname), dict(
    _rev=rev,
    language='javascript',
    views=dict(
      all_numbers=dict(
        map='function(doc) { if(doc.number) emit(null, doc) }',
      ),
      count_sum_numbers=dict(
        map='function(doc) { if(doc.number) emit(null, doc.number) }',
        reduce=dedent('''
                  function(key, values, rereduce) {
                    var sum = 0
                    var count = 0
                    for(var i = 0; i < values.length; i++) {
                      sum += values[i]
                      count += rereduce ? values.count : 1
                    }
                    return {count:count, sum:sum}
                  }
               ''')
      ),
    )
))

# Get the sum of all numbers
# Rows contains one row per key. Since we had only one (null) key, we have only one row
res = hget('%s/_design/%s/_view/count_sum_numbers' % (dbname, dbname)).data['rows'][0]['value']
print('Sum of all %d available numbers: %d' % (res['count'], res['sum']))