mirror of
https://github.com/internetarchive/warcprox.git
synced 2025-01-18 13:22:09 +01:00
playing with simple ORM thing
This commit is contained in:
parent
1ef9455885
commit
000e4d9cf6
@ -1,8 +1,10 @@
|
||||
language: python
|
||||
python:
|
||||
- 3.6
|
||||
- 3.5
|
||||
- 3.4
|
||||
- 2.7
|
||||
- 3.7-dev
|
||||
- nightly
|
||||
- pypy
|
||||
- pypy3
|
||||
|
@ -2,7 +2,7 @@
|
||||
rethinkstuff/__init__.py - rethinkdb connection-manager-ish thing and service
|
||||
registry thing
|
||||
|
||||
Copyright (C) 2015-2016 Internet Archive
|
||||
Copyright (C) 2015-2017 Internet Archive
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@ -215,3 +215,202 @@ class ServiceRegistry(object):
|
||||
except r.ReqlNonExistenceError:
|
||||
return []
|
||||
|
||||
class WatchedDict(dict):
|
||||
def __init__(self, d, callback, field):
|
||||
self.callback = callback
|
||||
self.field = field
|
||||
for key in d:
|
||||
dict.__setitem__(self, key, watch(
|
||||
d[key], callback=self.callback, field=self.field))
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
self.callback(self.field)
|
||||
return dict.__setitem__(self, key, watch(
|
||||
value, callback=self.callback, field=self.field))
|
||||
|
||||
def __delitem__(self, key):
|
||||
self.callback(self.field)
|
||||
return dict.__delitem__(self, key)
|
||||
|
||||
def clear(self):
|
||||
self.callback(self.field)
|
||||
return dict.clear(self)
|
||||
|
||||
def pop(self, *args):
|
||||
self.callback(self.field)
|
||||
return dict.pop(self, *args)
|
||||
|
||||
def popitem(self):
|
||||
self.callback(self.field)
|
||||
return dict.popitem()
|
||||
|
||||
def setdefault(self, *args):
|
||||
self.callback(self.field)
|
||||
if len(args) == 2:
|
||||
return dict.setdefault(self, args[0], watch(
|
||||
args[1], callback=self.callback, field=self.field))
|
||||
else:
|
||||
return dict.setdefault(self, *args)
|
||||
|
||||
def update(self, *args, **kwargs):
|
||||
# looks a little tricky
|
||||
raise Exception('not implemented')
|
||||
|
||||
class WatchedList(list):
|
||||
def __init__(self, l, callback, field):
|
||||
self.callback = callback
|
||||
self.field = field
|
||||
for item in l:
|
||||
list.append(self, watch(item, callback=callback, field=self.field))
|
||||
|
||||
def __setitem__(self, index, value):
|
||||
self.callback(self.field)
|
||||
return list.__setitem__(self, index, watch(
|
||||
value, callback=self.callback, field=self.field))
|
||||
|
||||
def __delitem__(self, index):
|
||||
self.callback(self.field)
|
||||
return list.__delitem__(self, index)
|
||||
|
||||
def append(self, value):
|
||||
self.callback(self.field)
|
||||
return list.append(self, watch(
|
||||
value, callback=self.callback, field=self.field))
|
||||
|
||||
def extend(self, value):
|
||||
self.callback(self.field)
|
||||
return list.extend(self, watch(
|
||||
list(value), callback=self.callback, field=self.field))
|
||||
|
||||
def insert(self, index, value):
|
||||
self.callback(self.field)
|
||||
return list.insert(self, index, watch(
|
||||
value, callback=self.callback, field=self.field))
|
||||
|
||||
def remove(self, value):
|
||||
self.callback(self.field)
|
||||
return list.remove(self, value)
|
||||
|
||||
def pop(self, index=-1):
|
||||
self.callback(self.field)
|
||||
return list.pop(self, index)
|
||||
|
||||
def clear(self):
|
||||
self.callback(self.field)
|
||||
return list.clear(self)
|
||||
|
||||
def sort(self, key=None, reverse=False):
|
||||
self.callback(self.field)
|
||||
return list.sort(self, key, reverse)
|
||||
|
||||
def reverse(self):
|
||||
self.callback(self.field)
|
||||
return list.reverse(self)
|
||||
|
||||
def watch(obj, callback, field):
|
||||
if isinstance(obj, dict):
|
||||
return WatchedDict(obj, callback, field)
|
||||
elif isinstance(obj, list):
|
||||
return WatchedList(obj, callback, field)
|
||||
else:
|
||||
return obj
|
||||
|
||||
class Document(dict, object):
|
||||
'''
|
||||
Base class for documents in rethinkdb.
|
||||
|
||||
You should subclass this class for each of your rethinkdb tables. You can
|
||||
add custom functionality in your subclass if appropriate.
|
||||
|
||||
This class keeps track of changes made to the object and any nested fields.
|
||||
After you have made some changes, call update() to persist them to the
|
||||
database.
|
||||
|
||||
Changes in nested fields result in updates to their first-level ancestor
|
||||
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'
|
||||
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.)
|
||||
'''
|
||||
def __init__(self, rethinker, d={}):
|
||||
dict.__setattr__(self, '_r', rethinker)
|
||||
for k in d:
|
||||
dict.__setitem__(
|
||||
self, k, watch(d[k], callback=self._updated, field=k))
|
||||
self._clear_updates()
|
||||
|
||||
def _clear_updates(self):
|
||||
dict.__setattr__(self, '_updates', {})
|
||||
dict.__setattr__(self, '_deletes', set())
|
||||
|
||||
def __setitem__(self, key, value):
|
||||
dict.__setitem__(
|
||||
self, key, watch(value, callback=self._updated, field=key))
|
||||
self._updated(key)
|
||||
|
||||
__setattr__ = __setitem__
|
||||
__getattr__ = dict.__getitem__
|
||||
|
||||
def __delitem__(self, key):
|
||||
dict.__delitem__(self, key)
|
||||
self._deletes.add(key)
|
||||
if key in self._updates:
|
||||
del self._updates[key]
|
||||
|
||||
# XXX do we need the other stuff like in WatchedDict?
|
||||
|
||||
def _updated(self, field):
|
||||
# callback for all updates
|
||||
self._updates[field] = self[field]
|
||||
if field in self._deletes:
|
||||
self._deletes.remove(field)
|
||||
|
||||
@property
|
||||
def table(self):
|
||||
'''
|
||||
Name of the rethinkdb table.
|
||||
|
||||
Defaults to the name of the class, lowercased. Can be overridden.
|
||||
'''
|
||||
return self.__class__.__name__.lower()
|
||||
|
||||
def table_create(self):
|
||||
'''
|
||||
Creates the table.
|
||||
|
||||
Subclasses may want to override to do more things, such as creating
|
||||
indexes.
|
||||
'''
|
||||
self._r.table_create(self.table).run()
|
||||
|
||||
def insert(self):
|
||||
result = self._r.table(self.table).insert(self).run()
|
||||
if 'generated_keys' in result:
|
||||
dict.__setitem__(self, 'id', result['generated_keys'][0])
|
||||
self._clear_updates()
|
||||
|
||||
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()
|
||||
|
||||
def refresh(self):
|
||||
'''
|
||||
Refresh from the database.
|
||||
'''
|
||||
d = self._r.table(self.table).get(self.id).run()
|
||||
for k in d:
|
||||
dict.__setitem__(
|
||||
self, k, watch(d[k], callback=self._updated, field=k))
|
||||
|
||||
|
||||
|
3
setup.py
3
setup.py
@ -3,12 +3,13 @@ import codecs
|
||||
|
||||
setuptools.setup(
|
||||
name='rethinkstuff',
|
||||
version='0.1.7',
|
||||
version='0.2.0.dev57',
|
||||
packages=['rethinkstuff'],
|
||||
classifiers=[
|
||||
'Programming Language :: Python :: 2.7',
|
||||
'Programming Language :: Python :: 3.4',
|
||||
'Programming Language :: Python :: 3.5',
|
||||
'Programming Language :: Python :: 3.6',
|
||||
],
|
||||
install_requires=['rethinkdb'],
|
||||
url='https://github.com/nlevitt/rethinkstuff',
|
||||
|
@ -1,7 +1,7 @@
|
||||
'''
|
||||
tests_rethinker.py - unit tests for rethinkstuff
|
||||
|
||||
Copyright (C) 2015-2016 Internet Archive
|
||||
Copyright (C) 2015-2017 Internet Archive
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@ -275,3 +275,89 @@ def test_utcnow():
|
||||
|
||||
## XXX what else can we test without jumping through hoops?
|
||||
|
||||
class SomeDoc(rethinkstuff.Document):
|
||||
pass
|
||||
|
||||
def test_orm(r):
|
||||
d = SomeDoc(rethinker=r, d={
|
||||
'a': 'b',
|
||||
'c': {'d': 'e'},
|
||||
'f': ['g', 'h'],
|
||||
'i': ['j', {'k': 'l'}]})
|
||||
|
||||
d.table_create()
|
||||
d.insert()
|
||||
|
||||
assert d._updates == {}
|
||||
d.m = 'n'
|
||||
assert d._updates == {'m': 'n'}
|
||||
d['c']['o'] = 'p'
|
||||
assert d._updates == {'m': 'n', 'c': {'d': 'e', 'o': 'p'}}
|
||||
d.f[0] = 'q'
|
||||
assert d._updates == {'m': 'n', 'c': {'d': 'e', 'o': 'p'}, 'f': ['q', 'h']}
|
||||
d['i'][1]['k'] = 's'
|
||||
assert d._updates == {
|
||||
'm': 'n',
|
||||
'c': {'d': 'e', 'o': 'p'},
|
||||
'f': ['q', 'h'],
|
||||
'i': ['j', {'k': 's'}]}
|
||||
|
||||
del d['i']
|
||||
assert d._deletes == {'i'}
|
||||
assert d._updates == {'m': 'n', 'c': {'d': 'e', 'o': 'p'}, 'f': ['q', 'h']}
|
||||
|
||||
d.i = 't'
|
||||
assert d._deletes == set()
|
||||
assert d._updates == {
|
||||
'm': 'n', 'c': {'d': 'e', 'o': 'p'}, 'f': ['q', 'h'], 'i': 't'}
|
||||
|
||||
d.f.append(['sublist'])
|
||||
assert d._updates == {
|
||||
'm': 'n', 'c': {'d': 'e', 'o': 'p'},
|
||||
'f': ['q', 'h', ['sublist']], 'i': 't'}
|
||||
|
||||
### list.clear not in python 2.7
|
||||
# d.f[2].clear()
|
||||
# assert d._updates == {
|
||||
# 'm': 'n', 'c': {'d': 'e', 'o': 'p'},
|
||||
# 'f': ['q', 'h', []], 'i': 't'}
|
||||
|
||||
result = d.f.pop()
|
||||
assert result == ['sublist']
|
||||
assert d._updates == {
|
||||
'm': 'n', 'c': {'d': 'e', 'o': 'p'},
|
||||
'f': ['q', 'h'], 'i': 't'}
|
||||
|
||||
del d.f[0]
|
||||
assert d._updates == {
|
||||
'm': 'n', 'c': {'d': 'e', 'o': 'p'},
|
||||
'f': ['h'], 'i': 't'}
|
||||
|
||||
d.f.insert(0, 'u')
|
||||
assert d._updates == {
|
||||
'm': 'n', 'c': {'d': 'e', 'o': 'p'},
|
||||
'f': ['u', 'h'], 'i': 't'}
|
||||
|
||||
d.f.extend(('v', {'w': 'x'}))
|
||||
assert d._updates == {
|
||||
'm': 'n', 'c': {'d': 'e', 'o': 'p'},
|
||||
'f': ['u', 'h', 'v', {'w': 'x'}], 'i': 't'}
|
||||
|
||||
# check that stuff added by extend() is watched properly
|
||||
d.f[3]['y'] = 'z'
|
||||
assert d._updates == {
|
||||
'm': 'n', 'c': {'d': 'e', 'o': 'p'},
|
||||
'f': ['u', 'h', 'v', {'w': 'x', 'y': 'z'}], 'i': 't'}
|
||||
|
||||
d.f.remove('h')
|
||||
assert d._updates == {
|
||||
'm': 'n', 'c': {'d': 'e', 'o': 'p'},
|
||||
'f': ['u', 'v', {'w': 'x', 'y': 'z'}], 'i': 't'}
|
||||
|
||||
expected = dict(d)
|
||||
d.update()
|
||||
assert d._updates == {}
|
||||
assert d._deletes == set()
|
||||
|
||||
d.refresh()
|
||||
assert d == expected
|
||||
|
Loading…
x
Reference in New Issue
Block a user