activitypub/activitypub/database/listdb.py

400 wiersze
15 KiB
Python

import re
import ast
import json
import copy
from ..bson import ObjectId
from .base import Database, Table
def is_match(lhs, rhs):
"""
>>> is_match(12, 12)
True
>>> is_match(12, 13)
False
>>> is_match(11, {"$lt": 12})
True
>>> is_match(13, {"$lt": 12})
False
>>> is_match({"a1": 1, "a2": 2, "b3": 3}, {"$regex": "^a"})
True
>>> is_match({"a1": 1, "a2": 2, "b3": 3}, {"$regex": "^b"})
True
>>> is_match({"a1": 1, "a2": 2, "b3": 3}, {"$regex": "^c"})
False
"""
if isinstance(rhs, dict):
matched = True
for item in rhs:
if item == "$regex":
matched = any([key for key in lhs if re.match(rhs[item], key)])
elif item == "$lt":
matched = lhs < rhs[item]
elif item == "$gt":
matched = lhs > rhs[item]
elif item == "$in":
if isinstance(lhs, list):
matched = any([left for left in lhs if left in rhs[item]])
else:
matched = lhs in rhs[item]
else:
raise Exception("unknown operator: %s" % item)
if not matched:
return False
return matched
else:
if isinstance(lhs, list):
if isinstance(rhs, list):
return lhs == rhs
else:
return rhs in lhs
else:
return lhs == rhs
class ListTable(Table):
def __init__(self, database=None, name=None, data=None):
super().__init__(database, name)
self.data = data or []
def __getitem__(self, item):
return self.data[item]
def __setitem__(self, item, value):
self.data[item] = value
def __delitem__(self, key):
del self.data[key]
def __len__(self):
return len(self.data)
def __str__(self):
return str(self.data)
def __repr__(self):
return repr(self.data)
def match(self, doc, query):
"""
Does a dictionary match a (possibly-nested) query/dict?
query is a dictionary of dotted words or query operations.
>>> table = ListTable()
>>> table.match({"x": 42}, {"x": 42})
True
>>> table.match({"x": 42}, {"x": 43})
False
>>> table.match({"x": {"y": 5}}, {"x.y": 5})
True
>>> table.match({"x": {"y": 5}}, {"x.y": 4})
False
>>> activity = {"name": "Joe"}
>>> table.match(activity, {"$and": [{"name": "Sally"}, {"name": "Joe"}]})
False
>>> table.match(activity, {"$and": [{"name": "Joe"}, {"name": "Sally"}]})
False
>>> table.match(activity, {"$or": [{"name": "Sally"}, {"name": "Joe"}]})
True
>>> table.match(activity, {"$or": [{"name": "Joe"}, {"name": "Sally"}]})
True
"""
for item in query:
if item == "$or":
matched = False
for each in query[item]:
if self.match(doc, each):
matched = True
break
if not matched:
return False
elif item == "$and":
matched = True
for each in query[item]:
if not self.match(doc, each):
matched = False
break
if not matched:
return False
else:
thing = self.get_item_in_dict(doc, item)
matched = is_match(thing, query[item])
if not matched:
return False
return True
def get_item_in_dict(self, dictionary, item):
"""
Get dictionary item from a dotted-word.
>>> table = ListTable()
>>> table.get_item_in_dict({"x": 1}, "x")
1
>>> table.get_item_in_dict({"x": {"y": 42}}, "x.y")
42
"""
current = dictionary
for word in item.split("."):
if word in current:
current = current[word]
else:
return None
return current
def set_item_in_dict(self, dictionary, item, value, i=None):
"""
Set dictionary item from a dotted-word.
>>> table = ListTable()
>>> d = {"x": 1}
>>> table.get_item_in_dict(d, "x")
1
>>> table.set_item_in_dict(d, "x", 2)
>>> table.get_item_in_dict(d, "x")
2
>>> d2 = {"x": {"y": 42}}
>>> table.get_item_in_dict(d2, "x.y")
42
>>> table.set_item_in_dict(d2, "x.y", 43)
>>> table.get_item_in_dict(d2, "x.y")
43
"""
current = dictionary
words = item.split(".")
for word in words[:-1]:
current = current[word]
current[words[-1]] = value
## update the database for those lists
## that are separate from the reference
## in dictionary (eg, redis)
if i is not None:
self.data[i] = dictionary
def clear(self):
self.data.clear()
def sort(self, sort_key, sort_order):
# sort_key = "_id"
# sort_order = 1 or -1
## Always use ListTable here:
return ListTable(data=sorted(
self.data,
key=lambda row: self.get_item_in_dict(row, sort_key),
reverse=(sort_order == -1)))
def insert_one(self, row):
"""
>>> table = ListTable()
>>> table.count()
0
>>> len(table.data)
0
>>> table.insert_one({"a": 1, "b": 2})
>>> len(table.data)
1
>>> table.count()
1
>>> table.insert_one({"c": 1, "d": 2})
>>> table.insert_one({"c": 1, "d": 2})
>>> table.insert_one({"c": 1, "d": 3})
>>> table.insert_one({"c": 1, "d": 3})
>>> table.find({"a": 1}).count()
1
>>> table.find({"a": 2}).count()
0
>>> table.find({"b": 2}).count()
1
>>> table.find({"c": 1}).count()
4
>>> table.find({"c": 1, "d": 2}).count()
2
"""
row = copy.deepcopy(row) # expensive, but prevents errors
if row.get("_id", None) is None:
row["_id"] = ObjectId()
self.data.append(row)
def find(self, query=None, limit=None, enumerated=False):
"""
>>> table = ListTable()
>>> table.insert_one({"a": 1, "b": 2})
>>> table.find({"a": 1}) # doctest: +ELLIPSIS
[{'a': 1, 'b': 2, '_id': ObjectId('...')}]
>>> table.find({"a": 2})
[]
"""
if query is not None:
if limit is not None:
if enumerated:
return [(i,doc) for (i,doc) in enumerate(self.data) if self.match(doc, query)][:limit]
else:
return ListTable(data=[doc for doc in self.data if self.match(doc, query)][:limit])
else:
if enumerated:
return [(i,doc) for (i,doc) in enumerate(self.data) if self.match(doc, query)]
else:
return ListTable(data=[doc for doc in self.data if self.match(doc, query)])
elif limit is not None:
if enumerated:
return list(enumerated(self.data[:limit]))
else:
return ListTable(data=self.data[:limit])
else:
if enumerated:
return list(enumerated(self.data))
else:
return self
def remove(self, query=None):
"""
>>> table = ListTable()
>>> table.insert_one({"a": 1, "b": 2})
>>> table.insert_one({"c": 3, "d": 4})
>>> table.find({"a": 1}) # doctest: +ELLIPSIS
[{'a': 1, 'b': 2, '_id': ObjectId('...')}]
>>> table.find({"a": 2})
[]
>>> table.find({"d": 4}) # doctest: +ELLIPSIS
[{'c': 3, 'd': 4, '_id': ObjectId('...')}]
>>> table.remove({"d": 4})
>>> table.find({"d": 4})
[]
>>> table.find({"b": 2}) # doctest: +ELLIPSIS
[{'a': 1, 'b': 2, '_id': ObjectId('...')}]
"""
if query:
items = [(i,doc) for (i,doc) in enumerate(self.data) if self.match(doc, query)]
for i,doc in items:
del self.data[i]
else:
self.data.clear()
def find_one(self, query):
"""
>>> table = ListTable()
>>> table.insert_one({"a": 1, "b": 2})
>>> table.insert_one({"a": 3, "b": 4})
>>> table.find_one({"b": 2}) # doctest: +ELLIPSIS
{'a': 1, 'b': 2, '_id': ObjectId('...')}
>>> table.find_one({"b": 3})
>>> table.find_one({"b": 4}) # doctest: +ELLIPSIS
{'a': 3, 'b': 4, '_id': ObjectId('...')}
"""
results = [doc for doc in self.data if self.match(doc, query)]
if results:
return results[0]
else:
return None
def find_one_and_update(self, query, items, sort=None):
one = self.find_one(query)
if len(one) > 0:
return False
else:
self.update_one(query, items)
return True
def update_one(self, query, items, upsert=False):
results = [(i,doc) for (i,doc) in enumerate(self.data) if self.match(doc, query)]
if len(results) > 0:
self.process_updates(results[:1], items)
elif upsert: ## update and insert
self.insert_one(query)
results = self.find(query, enumerated=True)
self.process_updates(results, items)
def process_updates(self, results, items):
"""
results is rows of matched data (dicts)
items are things like:
* {'$set': {'meta.deleted': True}}
* {'$set': {'meta.undo': True, 'meta.exta': 'object deleted'}}
* {'$inc': {'meta.count_reply': -1, 'meta.count_direct_reply': -1}}
>>> db = ListDatabase()
>>> db.actors.insert_one({'meta': {'deleted': True}})
>>> len(db.actors.find({'meta.deleted': True}))
1
>>> db.actors.process_updates(enumerate(db.actors.data),
... {"$set": {'meta.deleted': False}})
>>> len(db.actors.find({'meta.deleted': True}))
0
>>> len(db.actors.find({'meta.deleted': False}))
1
>>> db = ListDatabase()
>>> db.actors.insert_one({'meta': {'count': 0}})
>>> db.actors.find()[0]["meta"]["count"]
0
>>> db.actors.process_updates(enumerate(db.actors.data),
... {"$inc": {'meta.count': +1}})
>>> db.actors.find()[0]["meta"]["count"]
1
>>> db.actors.process_updates(enumerate(db.actors.data),
... {"$inc": {'meta.count': +1}})
>>> db.actors.find()[0]["meta"]["count"]
2
>>> db.actors.process_updates(enumerate(db.actors.data),
... {"$inc": {'meta.count': -1}})
>>> db.actors.find()[0]["meta"]["count"]
1
"""
for i,result in results:
for item in items: # key
if item == "$set":
for thing in items[item]: # keys of the $set
value = items[item][thing]
self.set_item_in_dict(result, thing, value, i)
elif item == "$inc":
for thing in items[item]:
old_value = self.get_item_in_dict(result, thing)
incr = items[item][thing]
self.set_item_in_dict(result, thing, old_value + incr, i)
def update(self, query, items, upsert=False):
"""
>>> db = ListDatabase()
>>> q = {"id": "XXX", "test": 42}
>>> db.actors.update(q, {"$inc": {"test": +1}}, upsert=False)
>>> len(db.actors.find(q))
0
>>> db.actors.update(q, {"$inc": {"test": +1}}, upsert=True)
>>> len(db.actors.find(q))
0
>>> q = {"id": "XXX", "test": 43}
>>> len(db.actors.find(q))
1
"""
results = [(i,doc) for (i,doc) in enumerate(self.data) if self.match(doc, query)]
if len(results) > 0:
self.process_updates(results, items)
elif upsert: # update and insert
self.insert_one(query)
results = self.find(query, enumerated=True)
self.process_updates(results, items)
def count(self, query=None):
if query:
return len([doc for doc in self.data if self.match(doc, query)])
else:
return len(self.data)
def count_documents(self, query):
return self.count(query)
class ListDatabase(Database):
Table = ListTable
"""
process_updates: [{'box': 'outbox', 'activity': {'type': 'Create', 'actor': 'http://localhost:5005', 'object': {'type': 'Note', 'sensitive': False, 'attributedTo': 'http://localhost:5005', 'cc': ['http://localhost:5005/followers'], 'to': ['https://www.w3.org/ns/activitystreams#Public'], 'content': '<p>2</p>', 'tag': [], 'published': '2018-07-19T17:23:22Z', 'id': 'http://localhost:5005/outbox/75b40a6f5c319bdf/activity', 'url': 'http://localhost:5005/note/75b40a6f5c319bdf'}, '@context': ['https://www.w3.org/ns/activitystreams', 'https://w3id.org/security/v1', {'Hashtag': 'as:Hashtag', 'sensitive': 'as:sensitive'}], 'published': '2018-07-19T17:23:22Z', 'to': ['https://www.w3.org/ns/activitystreams#Public'], 'cc': ['http://localhost:5005/followers'], 'id': 'http://localhost:5005/outbox/75b40a6f5c319bdf'}, 'type': ['Create'], 'remote_id': 'http://localhost:5005/outbox/75b40a6f5c319bdf', 'meta': {'undo': False, 'deleted': False}, '_id': ObjectId('5b50c90a1342a3318e13d434'), '_requested': True}]
{'$set': {'meta.deleted': True}}
done! [{'box': 'outbox', 'activity': {'type': 'Create', 'actor': 'http://localhost:5005', 'object': {'type': 'Note', 'sensitive': False, 'attributedTo': 'http://localhost:5005', 'cc': ['http://localhost:5005/followers'], 'to': ['https://www.w3.org/ns/activitystreams#Public'], 'content': '<p>2</p>', 'tag': [], 'published': '2018-07-19T17:23:22Z', 'id': 'http://localhost:5005/outbox/75b40a6f5c319bdf/activity', 'url': 'http://localhost:5005/note/75b40a6f5c319bdf'}, '@context': ['https://www.w3.org/ns/activitystreams', 'https://w3id.org/security/v1', {'Hashtag': 'as:Hashtag', 'sensitive': 'as:sensitive'}], 'published': '2018-07-19T17:23:22Z', 'to': ['https://www.w3.org/ns/activitystreams#Public'], 'cc': ['http://localhost:5005/followers'], 'id': 'http://localhost:5005/outbox/75b40a6f5c319bdf'}, 'type': ['Create'], 'remote_id': 'http://localhost:5005/outbox/75b40a6f5c319bdf', 'meta': {'undo': False, 'deleted': False}, '_id': ObjectId('5b50c90a1342a3318e13d434'), '_requested': True, 'm': 'e'}]
process_updates: [] {'$set': {'meta.undo': True, 'meta.exta': 'object deleted'}}
done! []
"""