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
|
language: python
|
||||||
python:
|
python:
|
||||||
|
- 3.6
|
||||||
- 3.5
|
- 3.5
|
||||||
- 3.4
|
- 3.4
|
||||||
- 2.7
|
- 2.7
|
||||||
|
- 3.7-dev
|
||||||
- nightly
|
- nightly
|
||||||
- pypy
|
- pypy
|
||||||
- pypy3
|
- pypy3
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
rethinkstuff/__init__.py - rethinkdb connection-manager-ish thing and service
|
rethinkstuff/__init__.py - rethinkdb connection-manager-ish thing and service
|
||||||
registry thing
|
registry thing
|
||||||
|
|
||||||
Copyright (C) 2015-2016 Internet Archive
|
Copyright (C) 2015-2017 Internet Archive
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
@ -215,3 +215,202 @@ class ServiceRegistry(object):
|
|||||||
except r.ReqlNonExistenceError:
|
except r.ReqlNonExistenceError:
|
||||||
return []
|
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(
|
setuptools.setup(
|
||||||
name='rethinkstuff',
|
name='rethinkstuff',
|
||||||
version='0.1.7',
|
version='0.2.0.dev57',
|
||||||
packages=['rethinkstuff'],
|
packages=['rethinkstuff'],
|
||||||
classifiers=[
|
classifiers=[
|
||||||
'Programming Language :: Python :: 2.7',
|
'Programming Language :: Python :: 2.7',
|
||||||
'Programming Language :: Python :: 3.4',
|
'Programming Language :: Python :: 3.4',
|
||||||
'Programming Language :: Python :: 3.5',
|
'Programming Language :: Python :: 3.5',
|
||||||
|
'Programming Language :: Python :: 3.6',
|
||||||
],
|
],
|
||||||
install_requires=['rethinkdb'],
|
install_requires=['rethinkdb'],
|
||||||
url='https://github.com/nlevitt/rethinkstuff',
|
url='https://github.com/nlevitt/rethinkstuff',
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
'''
|
'''
|
||||||
tests_rethinker.py - unit tests for rethinkstuff
|
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");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with 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?
|
## 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