mirror of
https://github.com/internetarchive/warcprox.git
synced 2025-01-18 13:22:09 +01:00
better smarter ORM with more tests
This commit is contained in:
parent
d76e219e7b
commit
abdecc46b8
78
README.rst
78
README.rst
@ -1,43 +1,69 @@
|
|||||||
.. image:: https://travis-ci.org/nlevitt/rethinkstuff.svg?branch=master
|
.. image:: https://travis-ci.org/nlevitt/rethinkstuff.svg?branch=master
|
||||||
:target: https://travis-ci.org/nlevitt/rethinkstuff
|
:target: https://travis-ci.org/nlevitt/rethinkstuff
|
||||||
|
|
||||||
rethinkstuff
|
rethinkstuff
|
||||||
============
|
============
|
||||||
|
|
||||||
Rudimentary rethinkdb python library with some smarts (and maybe some
|
RethinkDB python library. Provides connection manager and ORM framework
|
||||||
dumbs)
|
(object-relational mapping, sometimes called ODM or OM for nosql databases).
|
||||||
|
|
||||||
What? Why?
|
Connection Manager
|
||||||
----------
|
------------------
|
||||||
|
|
||||||
As of now there is a very small amount of code here. I had three
|
Three main purposes:
|
||||||
projects using the Rethinker class, and had enough code churn inside the
|
|
||||||
class that it became too painful to keep the three copies in sync. Thus,
|
|
||||||
a library shared among them.
|
|
||||||
|
|
||||||
Three main purposes:
|
- round-robin connections among database servers
|
||||||
|
- make sure connections close at proper time
|
||||||
- round-robin connections among database servers
|
|
||||||
- make sure connections close at proper time
|
|
||||||
- retry retry-able queries on failure
|
- retry retry-able queries on failure
|
||||||
|
|
||||||
Not really a connection pool, because it doesn’t keep any connections
|
Not currently a connection pool, because it doesn’t keep any connections open.
|
||||||
open, but it does take care of connection management.
|
Should be possible to implement connection pooling without changing the API.
|
||||||
|
|
||||||
Service Registry
|
Usage Example
|
||||||
~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~
|
||||||
|
|
||||||
Now also has a ServiceRegistry class, a lightweight solution for service
|
|
||||||
discovery for distributed services. Maintains service info and status in
|
|
||||||
a rethinkdb table called “services”.
|
|
||||||
|
|
||||||
Usage
|
|
||||||
-----
|
|
||||||
|
|
||||||
::
|
::
|
||||||
|
|
||||||
import rethinkstuff
|
import rethinkstuff
|
||||||
r = rethinkstuff.Rethinker(['db0.foo.com', 'db0.foo.com:38015', 'db1.foo.com'], 'my_db')
|
r = rethinkstuff.Rethinker(['db0.foo.com', 'db0.foo.com:38015', 'db1.foo.com'], 'my_db')
|
||||||
r.table('my_table').insert({'foo':'bar','baz':2}).run()
|
r.table('mytable').insert({'foo':'bar','baz':2}).run()
|
||||||
for result in r.table('my_table'):
|
for result in r.table('mytable'):
|
||||||
print("result={}".format(result))
|
print("result={}".format(result))
|
||||||
|
|
||||||
|
ORM
|
||||||
|
---
|
||||||
|
|
||||||
|
Simple yet powerful ORM system. *Does not enforce a schema.*
|
||||||
|
|
||||||
|
Usage Example
|
||||||
|
~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
::
|
||||||
|
|
||||||
|
import rethinkstuff
|
||||||
|
|
||||||
|
r = rethinkstuff.Rethinker(['db0.foo.com', 'db0.foo.com:38015', 'db1.foo.com'], 'my_db')
|
||||||
|
|
||||||
|
class MyTable(rethinkstuff.Document):
|
||||||
|
pass
|
||||||
|
MyTable.table_create()
|
||||||
|
|
||||||
|
doc1 = MyTable(r, {'animal': 'elephant', 'size': 'large'})
|
||||||
|
doc1.save()
|
||||||
|
|
||||||
|
doc1_copy = MyTable.get(r, doc1.id)
|
||||||
|
doc1_copy.food = 'bread'
|
||||||
|
doc1_copy.save()
|
||||||
|
|
||||||
|
doc1.first_name = 'Frankworth'
|
||||||
|
doc1.save()
|
||||||
|
|
||||||
|
doc1.refresh()
|
||||||
|
|
||||||
|
Service Registry
|
||||||
|
----------------
|
||||||
|
|
||||||
|
Now also has a ServiceRegistry class, a lightweight solution for service
|
||||||
|
discovery for distributed services. Maintains service info and status in
|
||||||
|
a rethinkdb table called “services”.
|
||||||
|
|
||||||
|
@ -18,6 +18,7 @@ limitations under the License.
|
|||||||
|
|
||||||
import rethinkdb as r
|
import rethinkdb as r
|
||||||
import logging
|
import logging
|
||||||
|
import rethinkstuff
|
||||||
|
|
||||||
class WatchedDict(dict):
|
class WatchedDict(dict):
|
||||||
def __init__(self, d, callback, field):
|
def __init__(self, d, callback, field):
|
||||||
@ -119,6 +120,13 @@ def watch(obj, callback, field):
|
|||||||
else:
|
else:
|
||||||
return obj
|
return obj
|
||||||
|
|
||||||
|
|
||||||
|
class classproperty(object):
|
||||||
|
def __init__(self, fget):
|
||||||
|
self.fget = fget
|
||||||
|
def __get__(self, owner_self, owner_cls):
|
||||||
|
return self.fget(owner_cls)
|
||||||
|
|
||||||
class Document(dict, object):
|
class Document(dict, object):
|
||||||
'''
|
'''
|
||||||
Base class for ORM.
|
Base class for ORM.
|
||||||
@ -134,16 +142,47 @@ class Document(dict, object):
|
|||||||
field. For example, if your document starts as {'a': {'b': 'c'}}, then
|
field. For example, if your document starts as {'a': {'b': 'c'}}, then
|
||||||
you run d['a']['x'] = 'y', then the update will replace the whole 'a'
|
you run d['a']['x'] = 'y', then the update will replace the whole 'a'
|
||||||
field. Nested field updates get too complicated any other way.
|
field. Nested field updates get too complicated any other way.
|
||||||
|
|
||||||
The primary key must be `id`, the rethinkdb default. (XXX we could find out
|
|
||||||
what the primary key is from the "table_config" system table.)
|
|
||||||
'''
|
'''
|
||||||
|
|
||||||
|
@classproperty
|
||||||
|
def table(cls):
|
||||||
|
'''
|
||||||
|
Returns default table name, which is the class name, lowercased.
|
||||||
|
|
||||||
|
Subclasses can override this default more simply:
|
||||||
|
|
||||||
|
class Something(rethinkstuff.Document):
|
||||||
|
table = 'my_table_name'
|
||||||
|
'''
|
||||||
|
return cls.__name__.lower()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get(cls, rethinker, pk):
|
||||||
|
'''
|
||||||
|
Retrieve an instance from the database.
|
||||||
|
'''
|
||||||
|
doc = cls(rethinker)
|
||||||
|
doc[doc.pk_field] = pk
|
||||||
|
doc.refresh()
|
||||||
|
return doc
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def table_create(cls, rethinker):
|
||||||
|
'''
|
||||||
|
Creates the table.
|
||||||
|
|
||||||
|
Can be run on an instance of the class: `my_doc.table_create
|
||||||
|
Subclasses may want to override this method to do more things, such as
|
||||||
|
creating indexes.
|
||||||
|
'''
|
||||||
|
rethinker.table_create(cls.table).run()
|
||||||
|
|
||||||
def __init__(self, rethinker, d={}):
|
def __init__(self, rethinker, d={}):
|
||||||
dict.__setattr__(self, '_r', rethinker)
|
dict.__setattr__(self, '_r', rethinker)
|
||||||
for k in d:
|
dict.__setattr__(self, '_pk', None)
|
||||||
dict.__setitem__(
|
|
||||||
self, k, watch(d[k], callback=self._updated, field=k))
|
|
||||||
self._clear_updates()
|
self._clear_updates()
|
||||||
|
for k in d:
|
||||||
|
self[k] = watch(d[k], callback=self._updated, field=k)
|
||||||
|
|
||||||
def _clear_updates(self):
|
def _clear_updates(self):
|
||||||
dict.__setattr__(self, '_updates', {})
|
dict.__setattr__(self, '_updates', {})
|
||||||
@ -163,7 +202,7 @@ class Document(dict, object):
|
|||||||
if key in self._updates:
|
if key in self._updates:
|
||||||
del self._updates[key]
|
del self._updates[key]
|
||||||
|
|
||||||
# XXX do we need the other stuff like in WatchedDict?
|
# XXX probably need the other stuff like in WatchedDict
|
||||||
|
|
||||||
def _updated(self, field):
|
def _updated(self, field):
|
||||||
# callback for all updates
|
# callback for all updates
|
||||||
@ -172,47 +211,84 @@ class Document(dict, object):
|
|||||||
self._deletes.remove(field)
|
self._deletes.remove(field)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def table(self):
|
def pk_field(self):
|
||||||
'''
|
'''
|
||||||
Name of the rethinkdb table.
|
Name of the primary key field as retrieved from rethinkdb table
|
||||||
|
metadata, 'id' by default. Should not be overridden. Override
|
||||||
Defaults to the name of the class, lowercased. Can be overridden.
|
`table_create` if you want to use a nonstandard field as the primary
|
||||||
|
key.
|
||||||
'''
|
'''
|
||||||
return self.__class__.__name__.lower()
|
if not self._pk:
|
||||||
|
try:
|
||||||
|
pk = self._r.db('rethinkdb').table('table_config').filter({
|
||||||
|
'db': self._r.dbname, 'name': self.table}).get_field(
|
||||||
|
'primary_key')[0].run()
|
||||||
|
dict.__setattr__(self, '_pk', pk)
|
||||||
|
except Exception as e:
|
||||||
|
raise Exception(
|
||||||
|
'problem determining primary key for table %s.%s: %s',
|
||||||
|
self._r.dbname, self.table, e)
|
||||||
|
return self._pk
|
||||||
|
|
||||||
def table_create(self):
|
@property
|
||||||
|
def pk_value(self):
|
||||||
'''
|
'''
|
||||||
Creates the table.
|
Value of primary key field.
|
||||||
|
|
||||||
Subclasses may want to override this method to do more things, such as
|
|
||||||
creating indexes.
|
|
||||||
'''
|
'''
|
||||||
self._r.table_create(self.table).run()
|
return getattr(self, self.pk_field)
|
||||||
|
|
||||||
def insert(self):
|
def save(self):
|
||||||
result = self._r.table(self.table).insert(self).run()
|
'''
|
||||||
if 'generated_keys' in result:
|
Saves
|
||||||
dict.__setitem__(self, 'id', result['generated_keys'][0])
|
'''
|
||||||
self._clear_updates()
|
should_insert = False
|
||||||
|
try:
|
||||||
|
self.pk_value # raise KeyError if unset
|
||||||
|
if self._updates:
|
||||||
|
# r.literal() to replace, not merge with, nested fields
|
||||||
|
updates = {field: r.literal(self._updates[field])
|
||||||
|
for field in self._updates}
|
||||||
|
query = self._r.table(self.table).get(
|
||||||
|
self.pk_value).update(updates)
|
||||||
|
result = query.run()
|
||||||
|
if result['skipped']: # primary key not found
|
||||||
|
should_insert = True
|
||||||
|
elif result['errors'] or result['deleted']:
|
||||||
|
raise Exception(
|
||||||
|
'unexpected result %s from rethinkdb query %s' % (
|
||||||
|
result, query))
|
||||||
|
if not should_insert and self._deletes:
|
||||||
|
self._r.table(self.table).replace(
|
||||||
|
r.row.without(self._deletes)).run()
|
||||||
|
if result['errors']: # primary key not found
|
||||||
|
should_insert = True
|
||||||
|
elif not result['replaced'] == 0:
|
||||||
|
raise Exception(
|
||||||
|
'unexpected result %s from rethinkdb query %s' % (
|
||||||
|
result, query))
|
||||||
|
except KeyError:
|
||||||
|
should_insert = True
|
||||||
|
|
||||||
|
if should_insert:
|
||||||
|
query = self._r.table(self.table).insert(self)
|
||||||
|
result = query.run()
|
||||||
|
if result['inserted'] != 1:
|
||||||
|
raise Exception(
|
||||||
|
'unexpected result %s from rethinkdb query %s' % (
|
||||||
|
result, query))
|
||||||
|
if 'generated_keys' in result:
|
||||||
|
dict.__setitem__(
|
||||||
|
self, self.pk_field, result['generated_keys'][0])
|
||||||
|
|
||||||
def update(self):
|
|
||||||
# hmm, masks dict.update()
|
|
||||||
if self._updates:
|
|
||||||
# r.literal() to replace, not merge with, nested fields
|
|
||||||
updates = {
|
|
||||||
field: r.literal(
|
|
||||||
self._updates[field]) for field in self._updates}
|
|
||||||
self._r.table(self.table).get(self.id).update(updates).run()
|
|
||||||
if self._deletes:
|
|
||||||
self._r.table(self.table).replace(
|
|
||||||
r.row.without(self._deletes)).run()
|
|
||||||
self._clear_updates()
|
self._clear_updates()
|
||||||
|
|
||||||
def refresh(self):
|
def refresh(self):
|
||||||
'''
|
'''
|
||||||
Refresh from the database.
|
Refresh from the database.
|
||||||
'''
|
'''
|
||||||
d = self._r.table(self.table).get(self.id).run()
|
d = self._r.table(self.table).get(self.pk_value).run()
|
||||||
|
if d is None:
|
||||||
|
raise KeyError
|
||||||
for k in d:
|
for k in d:
|
||||||
dict.__setitem__(
|
dict.__setitem__(
|
||||||
self, k, watch(d[k], callback=self._updated, field=k))
|
self, k, watch(d[k], callback=self._updated, field=k))
|
||||||
|
2
setup.py
2
setup.py
@ -3,7 +3,7 @@ import codecs
|
|||||||
|
|
||||||
setuptools.setup(
|
setuptools.setup(
|
||||||
name='rethinkstuff',
|
name='rethinkstuff',
|
||||||
version='0.2.0.dev59',
|
version='0.2.0.dev60',
|
||||||
packages=['rethinkstuff'],
|
packages=['rethinkstuff'],
|
||||||
classifiers=[
|
classifiers=[
|
||||||
'Programming Language :: Python :: 2.7',
|
'Programming Language :: Python :: 2.7',
|
||||||
|
@ -43,10 +43,14 @@ class RethinkerForTesting(rethinkstuff.Rethinker):
|
|||||||
@pytest.fixture(scope="module")
|
@pytest.fixture(scope="module")
|
||||||
def r():
|
def r():
|
||||||
r = RethinkerForTesting()
|
r = RethinkerForTesting()
|
||||||
result = r.db_create("my_db").run()
|
try:
|
||||||
|
r.db_drop("rethinkstuff_test_db").run()
|
||||||
|
except rethinkdb.errors.ReqlOpFailedError:
|
||||||
|
pass
|
||||||
|
result = r.db_create("rethinkstuff_test_db").run()
|
||||||
assert not r.last_conn.is_open()
|
assert not r.last_conn.is_open()
|
||||||
assert result["dbs_created"] == 1
|
assert result["dbs_created"] == 1
|
||||||
return RethinkerForTesting(db="my_db")
|
return RethinkerForTesting(db="rethinkstuff_test_db")
|
||||||
|
|
||||||
@pytest.fixture(scope="module")
|
@pytest.fixture(scope="module")
|
||||||
def my_table(r):
|
def my_table(r):
|
||||||
@ -275,18 +279,24 @@ def test_utcnow():
|
|||||||
|
|
||||||
## XXX what else can we test without jumping through hoops?
|
## XXX what else can we test without jumping through hoops?
|
||||||
|
|
||||||
class SomeDoc(rethinkstuff.Document):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def test_orm(r):
|
def test_orm(r):
|
||||||
|
class SomeDoc(rethinkstuff.Document):
|
||||||
|
table = 'some_doc'
|
||||||
|
|
||||||
|
SomeDoc.table_create(r)
|
||||||
|
with pytest.raises(Exception):
|
||||||
|
SomeDoc.table_create(r)
|
||||||
|
|
||||||
|
# test that overriding Document.table works
|
||||||
|
assert 'some_doc' in r.table_list().run()
|
||||||
|
assert not 'somedoc' in r.table_list().run()
|
||||||
|
|
||||||
d = SomeDoc(rethinker=r, d={
|
d = SomeDoc(rethinker=r, d={
|
||||||
'a': 'b',
|
'a': 'b',
|
||||||
'c': {'d': 'e'},
|
'c': {'d': 'e'},
|
||||||
'f': ['g', 'h'],
|
'f': ['g', 'h'],
|
||||||
'i': ['j', {'k': 'l'}]})
|
'i': ['j', {'k': 'l'}]})
|
||||||
|
d.save()
|
||||||
d.table_create()
|
|
||||||
d.insert()
|
|
||||||
|
|
||||||
assert d._updates == {}
|
assert d._updates == {}
|
||||||
d.m = 'n'
|
d.m = 'n'
|
||||||
@ -355,9 +365,66 @@ def test_orm(r):
|
|||||||
'f': ['u', 'v', {'w': 'x', 'y': 'z'}], 'i': 't'}
|
'f': ['u', 'v', {'w': 'x', 'y': 'z'}], 'i': 't'}
|
||||||
|
|
||||||
expected = dict(d)
|
expected = dict(d)
|
||||||
d.update()
|
d.save()
|
||||||
assert d._updates == {}
|
assert d._updates == {}
|
||||||
assert d._deletes == set()
|
assert d._deletes == set()
|
||||||
|
|
||||||
d.refresh()
|
d_copy = SomeDoc.get(r, d.id)
|
||||||
assert d == expected
|
assert d == d_copy
|
||||||
|
|
||||||
|
d['zuh'] = 'toot'
|
||||||
|
d.save()
|
||||||
|
assert d != d_copy
|
||||||
|
d_copy.refresh()
|
||||||
|
assert d == d_copy
|
||||||
|
|
||||||
|
def test_orm_pk(r):
|
||||||
|
class NonstandardPrimaryKey(rethinkstuff.Document):
|
||||||
|
@classmethod
|
||||||
|
def table_create(cls, rethinker):
|
||||||
|
rethinker.table_create(cls.table, primary_key='not_id').run()
|
||||||
|
|
||||||
|
with pytest.raises(Exception):
|
||||||
|
NonstandardPrimaryKey.get(r, 'no_such_thing')
|
||||||
|
|
||||||
|
NonstandardPrimaryKey.table_create(r)
|
||||||
|
|
||||||
|
# new empty doc
|
||||||
|
f = NonstandardPrimaryKey(r, {})
|
||||||
|
f.save()
|
||||||
|
assert f.pk_value
|
||||||
|
assert 'not_id' in f
|
||||||
|
assert f.not_id == f.pk_value
|
||||||
|
assert len(f.keys()) == 1
|
||||||
|
|
||||||
|
with pytest.raises(KeyError):
|
||||||
|
NonstandardPrimaryKey.get(r, 'no_such_thing')
|
||||||
|
|
||||||
|
# new doc with (only) primary key
|
||||||
|
d = NonstandardPrimaryKey(r, {'not_id': 1})
|
||||||
|
assert d.not_id == 1
|
||||||
|
assert d.pk_value == 1
|
||||||
|
d.save()
|
||||||
|
|
||||||
|
d_copy = NonstandardPrimaryKey.get(r, 1)
|
||||||
|
assert d == d_copy
|
||||||
|
|
||||||
|
# new doc with something in it
|
||||||
|
e = NonstandardPrimaryKey(r, {'some_field': 'something'})
|
||||||
|
with pytest.raises(KeyError):
|
||||||
|
e.not_id
|
||||||
|
with pytest.raises(KeyError):
|
||||||
|
e['not_id']
|
||||||
|
e.save()
|
||||||
|
assert e.not_id
|
||||||
|
|
||||||
|
e_copy = NonstandardPrimaryKey.get(r, e.not_id)
|
||||||
|
assert e == e_copy
|
||||||
|
e_copy['blah'] = 'toot'
|
||||||
|
e_copy.save()
|
||||||
|
|
||||||
|
e.refresh()
|
||||||
|
assert e['blah'] == 'toot'
|
||||||
|
assert e == e_copy
|
||||||
|
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user