Sleepy.Mongoose: A MongoDB HTTP Interface

The first half of the MongoDB book is due this week, so I wrote a REST interface for Mongo (I’m a prolific procrastinator).  Anyway, it’s called Sleepy.Mongoose and it’s available at https://github.com/10gen-labs/sleepy.mongoose.

Installing Sleepy.Mongoose

  1. Install MongoDB.
  2. Install the Python driver:
    $ easy_install pymongo
  3. Download Sleepy.Mongoose.
  4. From the mongoose directory, run:
    $ python httpd.py

You’ll see something that looks like:

=================================
|      MongoDB REST Server      |
=================================

listening for connections on http://localhost:27080

Using Sleepy.Mongoose

First, we’re just going to ping Sleepy.Mongoose to make sure it’s awake. You can use curl:

$ curl 'http://localhost:27080/_hello'

and it’ll send back a Star Wars quote.

To really use the interface, we need to connect to a database server. To do this, we post our database server address to the URI “/_connect” (all actions start with an underscore):

$ curl --data server=localhost:27017 'http://localhost:27080/_connect'

This connects to the database running at localhost:27017.

Now let’s insert something into a collection.

$ curl --data 'docs=[{"x":1}]' 'http://localhost:27080/foo/bar/_insert'

This will insert the document {“x” : 1} into the foo database’s bar collection. If we open up the JavaScript shell (mongo), we can see the document we just added:

> use foo
> db.bar.find()
{ "_id" : ObjectId("4b7edc9a1d41c8137e000000"), "x" : 1 }

But why bother opening the shell when we can query with curl?

$ curl -X GET 'http://localhost:27080/foo/bar/_find'
{"ok": 1, "results": [{"x": 1, "_id": {"$oid": "4b7edc9a1d41c8137e000000"}}], "id": 0}

Note that queries are GET requests, whereas the other requests up to this point have been posts (well, the _hello can be either).

A query returns three fields:

  • “ok”, which will be 1 if the query succeeded, 0 otherwise
  • “results” which is an array of documents from the db
  • “id” which is an identifier for that particular query

In this case “id” is irrelevant as we only have one document in the collection but if we had a bunch, we could use the id to get more results (_find only returns the first 15 matching documents by default, although it’s configurable). This will probably be clearer with an example, so let’s add some more documents to see how this works:

$ curl --data 'docs=[{"x":2},{"x":3}]' 'http://localhost:27080/foo/bar/_insert'
{"ok" : 1}

Now we have three documents. Let’s do a query and ask for it to return one result at a time:

$ curl -X GET 'http://localhost:27080/foo/bar/_find?batch_size=1'
{"ok": 1, "results": [{"x": 1, "_id": {"$oid": "4b7edc9a1d41c8137e000000"}}], "id": 1}

The only difference between this query and the one above is the “?batch_size=1″ which means “send one document back.” Notice that the cursor id is 1 now, too (not 0). To get the next result, we can do:

$ curl -X GET 'http://localhost:27080/foo/bar/_more?id=1&batch_size=1'
{"ok": 1, "results": [{"x": 2, "_id": {"$oid": "4b7ee0731d41c8137e000001"}}], "id": 1}
$ curl -X GET 'http://localhost:27080/foo/bar/_more?id=1&batch_size=1'
{"ok": 1, "results": [{"x": 3, "_id": {"$oid": "4b7ee0731d41c8137e000002"}}], "id": 1}

Now let’s remove a document:

$ curl --data 'criteria={"x":2}' 'http://localhost:27080/foo/bar/_remove'
{"ok" : 1}

Now if we do a _find, it only returns two documents:

$ curl -X GET 'http://localhost:27080/foo/bar/_find'
{"ok": 1, "results": [{"x": 1, "_id": {"$oid": "4b7edc9a1d41c8137e000000"}}, {"x": 3, "_id": {"$oid": "4b7ee0731d41c8137e000002"}}], "id": 2}

And finally, updates:

$ curl --data 'criteria={"x":1}&newobj={"$inc":{"x":1}}' 'http://localhost:27080/foo/bar/_update'

Let’s do a _find to see the updated object, this time using criteria: {“x”:2}. To put this in a URL, we need to escape the ‘{‘ and ‘}’ characters. You can do this by copy-pasting it into any javascript interpreter (Rhino, Spidermonkey, mongo, Firebug, Chome’s dev tools) as follows:

> escape('{"x":2}')
%7B%22x%22%3A2%7D

And now we can use that in our URL:

$ curl -X GET 'http://localhost:27080/foo/bar/_find?criteria=%7B%22x%22%3A2%7D'
{"ok": 1, "results": [{"x": 2, "_id": {"$oid": "4b7edc9a1d41c8137e000000"}}], "id": 0}

If you’re looking to go beyond the basic CRUD, there’s more documentation in the wiki.

This code is super-alpha. Comments, questions, suggestions, patches, and forks are all welcome.

Note: Sleepy.Mongoose is an offshoot of something I’m actually supposed to be working on: a JavaScript API we’re going to use to make an awesome sharding tool.  Administrating your cluster will be a point-and-click interface.  You’ll be able to see how everything is doing, drag n’ drop chunks, visually split collections… it’s going to be so cool.

  • Ivansergeev

    :-(
    Its work in mongo shell (db.bar.find({“query”:{},”$explain”:true});)
    and dont work in mongoose
    curl -X GET ‘http://localhost:27080/foo/bar/_find?criteria=%7B%22query%22%3AD B%7D%2C%22%24explain%22%3Atrue%7D’
    (criteria={“query”:{},”$explain”:true})
    result is empty: {“ok”: 1, “results”: [], “id”: 30}

  • Ivansergeev

    And one more question: date is work?
    http://localhost:27080/foo/bar/_insert?docs=%7B%22my_date%22%3A%20%7B%22%24date%22%3A%201290948386%7D%20%7D

    where docs={“my_date”: {“$date”: 1290948386} }

    return error: {“ok” : 0, “errmsg” : “100-continue msgs not handled yet”}

    (mac osx 10.6.4 – 64, mongodb 1.6.4, pymongo 1.8, latest Sleepy.Mongoose)

  • Anonymous

    Sorry, you’re right. Python driver’s fault :-P

    I just added support for it if you download the latest code. Do:

    curl 'http://localhost:27080/foo/bar/_find?explain=true'
    
  • Ivansergeev

    Thank you!

  • http://cogarch.wordpress.com/ Brian Burns

    Just to note, the quote handling for curl is a bit different on Windows:

    > curl ‘http://localhost:27080/_hello’
    curl: (1) Unsupported protocol: ‘http

    > curl http://localhost:27080/_hello
    {“ok” : 1, “msg” : “Uh, we had a slight weapons malfunction, but uh… everything’s perfectly all right now. We’re fine. We’re all fine here now, thank you. How are you?”}

    > curl –data server=localhost:27017 http://localhost:27080/_connect
    {“ok” : 1, “host” : “localhost”, “port” : 27017, “name” : “default”}

    > curl –data docs=[{"x":1}] http://localhost:27080/foo/bar/_insert
    {“oids”: [{"$oid": "4d0ae6b1efe8e96d90000003"}]}

    >curl -X GET http://localhost:27080/foo/bar/_find?batch_size=1
    {“ok”: 1, “results”: [{"x": 1, "_id": {"$oid": "4d0ae545efe8e96d90000001"}}], “id”: 2}

    > curl -V
    curl 7.16.4 (i686-pc-mingw32) libcurl/7.16.4 zlib/1.2.3
    Protocols: tftp ftp telnet dict ldap http file
    Features: Largefile libz

  • sebas

    Hello, how can I start the webservice automatically at linux startup? Thanks for any help.

  • Anonymous

    It’s not really set up to run as a service, although it shouldn’t be hard to add.

  • sebas

    thanks for the reply. Do you think this web service could be used in production for very simple applications (simple facebook app)?

  • sebas

    thanks for the reply. Do you think this web service could be used in production for very simple applications (simple facebook app)?

  • Sagar Ponkshe

    Hi Kristina, thanks for the great help. This is my second day with MongoDB. I could successfully run the sleepymongoose via HTML/JS. Could you please tell me how to store the response in a JS variable instead of txt file with JSON object? Sorry for asking such a naive question. -sagar

  • Joe

    Wow this is very cool. Thank you for taken the time to write this. I was wondering how I can use this with an Flash AIR application. I need my application to connect directly to a local MongoDb and also connect to a remote Master MongoDB. I will have several AIR client applications that will need to receive db updates from this master remote db but also be able to run off the local db if the connection to the master db is lost.

    I look forward to you comments.

    Joe

  • Joe

    Wow this is very cool. Thank you for taken the time to write this. I was wondering how I can use this with an Flash AIR application. I need my application to connect directly to a local MongoDb and also connect to a remote Master MongoDB. I will have several AIR client applications that will need to receive db updates from this master remote db but also be able to run off the local db if the connection to the master db is lost.

    I look forward to you comments.

    Joe

  • Anonymous

    Yes, it should work fine for that.

  • Anonymous

    Sleepy.Mongoose does not manage replica set connections for you, but it certainly lets you connect to multiple servers and manage them on your own.

  • Amol

    Hi there, my HTML POST query: http://localhost:27080/profilingdb/OMNTCollection/_find?UniqueWebId=12345ABCDE-12345ABCDE&server=localhost&name=profilingdb

    is not returning a specific record for “UniqueWebId=12345ABCDE-12345ABCDE”. Am I doing anything silly? Thanks for the help

  • Anonymous
  • http://www.facebook.com/rayie Raymond Ie

    You can try using upstart (if your distro supports it). It’s been working well for me.
    http://kevin.vanzonneveld.net/techblog/article/run_nodejs_as_a_service_on_ubuntu_karmic/
    is the tutorial for node but you can replace the ” node … ” part with ” python httpd.py ” and it’ll work.

  • http://www.facebook.com/rayie Raymond Ie

    Thanks for building this. It’s been working great for me alongside node.js
    I haven’t been able to get the native node/mongo driver to work so I’ve been using this as an alternative. I’ve had no issues with query,insert, or update/upsert so far…even with query batch_size > 100. Awesome.

  • Anonymous

    Thank you for letting me know! I’m so glad it’s helpful!

  • http://profiles.google.com/mduvall Matt Duvall

    Hi Kristina,

    Like Will above, I am also trying to access Sleepy.Mongoose using jQuery getJSON() method and have found the same cross-domain issue. Are there plans to update Sleepy.mongoose to be JSONP compliant? If not, do you have a method of using jQuery getJSON() that might get around the cross-domain issue? Thanks for your help.

    Matt

  • http://profiles.google.com/valeriano.cossu Valeriano Cossu

    That’s incredible.. :)

  • Amol

    Thanks Kristina. It helped.

  • Amol

    Hi Kristina,

    Like many other people here, I’m stuck at JSONP usage to overcome cross-domain issue. The response header says 200 OK, however the response is empty. Is there ANY work-around to this? Greatly appreciate the help. Thank you!

    -Amol

  • Amol

    Hi Kristina, I’m unable to find any related documentation; is there any development to support JSONP? Thanks.

  • bryce656

    I was needing to use group so I added the following to handlers.py. I am sure there are lots of improvements that can be made. I have never really messed with python much, so I just copied the find method and made it work for group. The main problem is the reduce and finalize functions. I am doing everything in js and since stringify doesn’t work with functions I am just passing them in as strings. Ex. reduce=”function(obj,prev){ prev.count%2B%2B }”

    def _group(self, args, out, name = None, db = None, collection = None):
    “”"
    query the database.
    “”"

    if type(args).__name__ != ‘dict’:
    out(‘{“ok” : 0, “errmsg” : “_find must be a GET request”}’)
    return

    conn = self._get_connection(name)
    if conn == None:
    out(‘{“ok” : 0, “errmsg” : “couldn’t get connection to mongo”}’)
    return

    if db == None or collection == None:
    out(‘{“ok” : 0, “errmsg” : “db and collection must be defined”}’)
    return

    key = {}
    if ‘key’ in args:
    key = self._get_son(args['key'][0], out)
    if key == None:
    return

    initial = {}
    if ‘initial’ in args:
    initial = self._get_son(args['initial'][0], out)
    if initial == None:
    return

    reduce = None
    if ‘reduce’ in args:
    reduce = args['reduce'][0]

    finalize = None
    if ‘finalize’ in args:
    finalize = args['finalize'][0]

    cond = {}
    if ‘cond’ in args:
    cond = self._get_son(args['cond'][0], out)
    if cond == None:
    return

    data = conn[db][collection].group(key=key, condition=cond, initial=initial, reduce=reduce, finalize=finalize)

    out(json.dumps({“results” : data, “ok” : 1}, default=json_util.default))

  • Renny

    Just an FYI: You’re returning “text/json” instead of “application/json” for content encoding. It’s a simple fix in httpd.py…

  • Anonymous

    Thanks, fixed!

  • Anonymous

    Don’t forget: group is just a database command, so you can also use https://github.com/kchodorow/sleepy.mongoose/wiki/Database-Commands to execute it.

  • Pingback: 第3回 MongoDB勉強会 ダイジェスト | cloudrop

  • http://twitter.com/chedim Дмитрий Чижевский

    it is NOT a rest interface >_<.

  • http://twitter.com/chedim Дмитрий Чижевский

    it is NOT a rest interface >_<.

  • Anonymous

    Yup, it’s not.  Interestingly, you’re the only person in a year and a half to notice that (or at least point it out). 

    Most people, when they say they want a REST interface, just mean HTTP :-/  

  • Trblft

    Great Job, I’m using it for Javascript MVC combining Backbone.js and JqueryUI, jstree…
    Thought I may need your help to make a “distinct” request possible via GET but I found out it was possible using “cmd”.

  • Kumar

    when i send the curl it fails with the following error:

    —————————————-Exception happened during processing of request from (’132.188.71.10′, 9033)Traceback (most recent call last):  File “/usr/lib64/python2.4/SocketServer.py”, line 222, in handle_request    self.process_request(request, client_address)  File “/usr/lib64/python2.4/SocketServer.py”, line 241, in process_request    self.finish_request(request, client_address)  File “/usr/lib64/python2.4/SocketServer.py”, line 254, in finish_request    self.RequestHandlerClass(request, client_address, self)  File “/usr/lib64/python2.4/SocketServer.py”, line 521, in __init__    self.handle()  File “/usr/lib64/python2.4/BaseHTTPServer.py”, line 316, in handle    self.handle_one_request()  File “/usr/lib64/python2.4/BaseHTTPServer.py”, line 310, in handle_one_request    method()  File “httpd.py”, line 179, in do_GET    (uri, args, type) = self.process_uri(“GET”)  File “httpd.py”, line 146, in process_uri    (uri, q, args) = self.path.partition(‘?’)AttributeError: ‘str’ object has no attribute ‘partition’—————————————-

    What am i doing wrong

  • Anonymous

    What version of Python are you using?  partition() didn’t exist until Python 2.5.

  • Booyanach

    is there any possibility for you to add a _serverStatus command?

    I’m absolutelly innapt at python to try and hack a solution to it right now.or, if it’s capable of doing it, could you demonstrate? is it with the _cmd?

  • Anonymous

    You can do this with a database command: https://github.com/kchodorow/sleepy.mongoose/wiki/Database-Commands:

    $ curl –data ‘cmd={“serverStatus” : 1}’ ‘http://localhost:27080/admin/_cmd’

  • Booyanach

    Thanks for the quick reply, but I end up with a problem,

    it gets me:
    {“ok” : 0, “errmsg” : “couldn’t parse json: None”}
    when I do it exactly as you wrote right there…
    I am on windows btw, is it possible that that’s the causer?

  • Anonymous

    I am not familiar with curl on Windows, sorry.  Looks like maybe it takes different arguments, that error message means it’s not getting any request parameters (“None”).

  • Booyanach

    well I got a bit further into it, thanks to a previous post here on the way that curl on windows handles the ”s on the commandline (basically it’s the same as you had without the ”s)
    now however am getting a different error:
    c:curl>curl –data cmd={“serverStatus”:1} http://localhost:27080/admin/_cmd{“ok” : 0, “errmsg” : “couldn’t parse json: {serverStatus:1}”}

  • Anonymous

    You need it to get to the server with double quotes (“s) around “serverStatus” (note it’s {serverStatus:1}, not {“serverStatus”:1}).  See the “Warning: Picky JSON Formatting Ahead” section of the documentation: https://github.com/kchodorow/sleepy.mongoose/wiki.

  • Booyanach

    now I feel dumb…
    forgot to escape the “”‘s xD
    Thanks alot for your time!
    now to get this to work with ajax… tad of a problem since I can’t do posts with jsonp xD

  • Anonymous

    Glad it’s working :)

    Someone actually just patched sleepy.mongoose to do JSONP, if you download the latest code from Github it should work.

  • Booyanach

    I am using the latest code :) started today with it actually,

    the problem I’m having however is when sending the _cmd as data in an ajax POST I don’t get anything returned to me, since JSONP doesn’t really handle POST cross-domain, unless it’s by a form.
    to better demonstrate here’s my code:$.ajax({ url: query, type: ‘POST’, data: cmd=nomes, dataType: “jsonp”, success: function(response) { console.log(response); }});

  • Gonçalo Vieira

    back once again ^_^;
    is there any thing that I could change in the source of sleepy mongoose to make _cmd work through a get command? this way I wouldn’t have to think about the same-domain policy of javascript, and I’d be pretty much saved.

  • Anonymous

    See https://github.com/kchodorow/sleepy.mongoose/commit/c2455f88bac0481a63718992f32648e63e4b6f7b.  You might want to message velniukas about it. 

    You cannot change _cmd to a GET.  It is a POST because commands can write to the database.  sleepy.mongoose is already pretty non-RESTy, I don’t want to make it even more so.

  • Anonymous

    See https://github.com/kchodorow/sleepy.mongoose/commit/c2455f88bac0481a63718992f32648e63e4b6f7b.  You might want to message velniukas about it. 

    You cannot change _cmd to a GET.  It is a POST because commands can write to the database.  sleepy.mongoose is already pretty non-RESTy, I don’t want to make it even more so.

  • Pingback: SleepyMongoose meets the Python after dark « Agile Developer's Blog

  • Scott Faisal

    Kristina, awesome job with this. Are you planning to continue development on this project?

  • Anonymous

    Thank you!

    Yes, I try to respond to peoples’ questions and fix bugs as they come in.  It is a side project and not an “officially supported” driver, so it’s not exactly a priority, but I try to keep it usable.

kristina chodorow's blog