Skip to content

Commit

Permalink
refactored pkg structure and added basic tests
Browse files Browse the repository at this point in the history
  • Loading branch information
jdp committed Apr 28, 2012
1 parent 78083fc commit 6bdceba
Show file tree
Hide file tree
Showing 8 changed files with 78 additions and 114 deletions.
24 changes: 4 additions & 20 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,21 +1,5 @@
.SUFFIXES: .coffee .js
COFFEE = coffee
SRC = lib/store.coffee lib/server.coffee lib/index.coffee
OBJ = ${SRC:.coffee=.js}
init:
pip install -r requirements.txt

# Stuff for compiling the PEG for simpler query language
PEGJS = pegjs
PEGSRC = lib/query_parser.pegjs

all: $(OBJ) peg

%.js: %.coffee
$(COFFEE) -cb $<

peg: $(PEGSRC)
$(PEGJS) $<

clean:
-rm lib/*.js

.PHONY: all clean peg
test:
nosetests tests
78 changes: 2 additions & 76 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,77 +1,3 @@
# Taxon: Redis-backed tagged data store

# TaxonDB

_Note that this is a prototype implemented with NodeJS and CoffeeScript. Actual implementation is likely to change._

TaxonDB is a small data store exposing tagged data over HTTP as JSON that allows arbitrary boolean queries supporting `and`, `or`, and `not` operations on those tags.

## RESTful API

TaxonDB's server exposes its data over HTTP, and by default runs on port 8980. All of the current API is namespaced under the `/v1` path. Two resources are exposed, the tags resource and the items resource, which are available under `/v1/tags` and `/v1/items` respectively.

### Tags Endpoints

#### Fetch All Tags

Endpoint: `GET /v1/tags`

A list of each unique tag is available by making this request. The list is kept up to date during item addition and removal.

Responses:

* `200` OK.

### Items Endpoints

All objects returned in the items endpoints share the same structure. A TaxonDB "item" is an object with three properties: an `_id`, which is used as a unique identifier; `data`, arbitrary string data attached to that item; and `tags`, an array of each tag associated with the item.

{
_id: "probably-a-uuid",
data: "arbitrary string data",
tags: ["set", "of", "tags"]
}

#### Fetch All Items

Endpoint: `GET /v1/items`

With no filters, this will return a blob of ever single item in the database. Queries can be used to filter the result set according to the tag relationships on the items.

Parameters:

* `query` An arbitrary boolean tag query. _(optional)_

Responses:

* `200` OK.
* `400`. Bad Request. Likely because of a malformed query.

#### Fetch Individual Item

Endpoint: `GET /v1/items/:id`

Making a request to this endpoint will return the item with the specified `id`.

Responses:

* `200` OK.
* `404` Not Found.

#### Create an Item

Endpoint: `POST /v1/items`

Creating an item will add it to the index and it will immediately show in subsequent queries.

Parameters:

* `id` The unique identifier of the item. _(required)_
* `data` Arbitrary data associated with the item. _(required)_
* `tag` Tag associated with the data. To add multiple tags, provide multiple `tag` parameters. _(required)_

Responses:

* `201` Created.
* `400` Bad Request. Likely because of a missing required parameter.

_Caveat: The NodeJS query string and form data parser will automatically convert multiple instances of the same field name into an array. This may be uncommon behavior, as it is common in web forms, other web frameworks, and functions like PHP's `http_build_query` to append and use index fields when encoding and reading arrays in query strings and form data. It is likely that the `tag` parameter will become `tags` and tags will be provided in a comma-separated string._
Taxon is a tagged data store with persistence to a Redis backend.
5 changes: 5 additions & 0 deletions taxon/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from .core import Store

__author__ = "Justin Poliey <justin@getglue.com>"
__license__ = "MIT"
__version__ = ".".join(map(str, (0, 1, 0)))
25 changes: 7 additions & 18 deletions core.py → taxon/core.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import hashlib
from functools import partial
import redis
import query
from .query import Query, sexpr


class Store(object):
Expand Down Expand Up @@ -38,7 +37,7 @@ def put(self, tag, items):
def _raw_query(self, fn, args):
"Perform a raw query on the Taxon store."

h = hashlib.sha1(query.sexpr({fn: args[:]}))
h = hashlib.sha1(sexpr({fn: args[:]}))
keyname = self._result_key(h.hexdigest())
if self.r.exists(keyname):
return (keyname, self.r.smembers(keyname))
Expand Down Expand Up @@ -73,25 +72,15 @@ def _raw_query(self, fn, args):
def query(self, q):
"Perform a query on the Taxon store."

if isinstance(q, query.Query):
if isinstance(q, Query):
return self._raw_query(*q.freeze().items()[0])
elif isinstance(q, dict):
return self._raw_query(*q.items()[0])
else:
raise TypeError("%s is not a recognized Taxon query" % q)

if __name__ == '__main__':
from query import *
def tags(self):
return self.r.smembers(self._make_key(self.tags_key))

r = redis.Redis(db=9)
s = Store(r)
s.put('foo', ['a', 'b'])
s.put('bar', ['c', 'a'])
s.put('baz', 'a')

_, items = s.query(Tag("foo") & ~Tag("baz"))
print items
_, items = s.query(And(Not("baz"), "foo"))
print items
_, items = s.query({'and': [{'tag': ['foo']}, {'or': [{'tag': ['fuck']}, {'not': [{'tag': ['baz']}]}]}]})
print items
def items(self):
return self.r.smembers(self._make_key(self.items_key))
File renamed without changes.
File renamed without changes.
5 changes: 5 additions & 0 deletions tests/context.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import os
import sys
sys.path.insert(0, os.path.abspath('..'))

import taxon
55 changes: 55 additions & 0 deletions tests/test_basic.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
from redis import Redis
from nose.tools import with_setup, eq_
from .context import taxon
from taxon.query import *

t = None


def setup():
global t
t = taxon.Store(Redis(db=9))


def teardown():
global t
t.r.flushdb()


@with_setup(teardown=teardown)
def simple_add_test():
t.put('foo', 'a')
t.put('bar', ['b', 'c'])
eq_(t.tags(), set(['foo', 'bar']))
eq_(t.items(), set(['a', 'b', 'c']))


@with_setup(teardown=teardown)
def tag_query_test():
t.put('foo', ['a', 'b', 'c'])
_, items = t.query(Tag("foo"))
eq_(items, set(['a', 'b', 'c']))


@with_setup(teardown=teardown)
def and_query_test():
t.put('foo', ['a', 'b'])
t.put('bar', ['a', 'c'])
_, items = t.query(And('foo', 'bar'))
eq_(items, set(['a']))


@with_setup(teardown=teardown)
def or_query_test():
t.put('foo', ['a', 'b'])
t.put('bar', ['a', 'c'])
_, items = t.query(Or('foo', 'bar'))
eq_(items, set(['a', 'b', 'c']))


@with_setup(teardown=teardown)
def not_query_test():
t.put('foo', ['a', 'b'])
t.put('bar', ['a', 'c'])
_, items = t.query(Not('foo'))
eq_(items, set(['c']))

0 comments on commit 6bdceba

Please sign in to comment.