Refactor database; added MongoDatabase

master
Douglas Blank 2018-07-19 09:45:11 -04:00
rodzic a0d6b39a40
commit 851af921fa
9 zmienionych plików z 190 dodań i 140 usunięć

Wyświetl plik

@ -9,7 +9,8 @@ This module is designed to be a generally useful ActivityPub library in Python.
The first two levels can be used indpendently, or together. They can best be used toegether using a Manager:
```python
>>> from activitypub import DummyDatabase, Manager
>>> from activitypub import Manager
>>> from activitypub.database import DummyDatabase
>>> db = DummyDatabase()
>>> manager = Manager(database=db)
>>> p = manager.Person(id="alyssa")

Wyświetl plik

@ -1,2 +1,2 @@
from .classes import Manager
from .database import Database, ListDatabase
from .manager import Manager
from .classes import *

Wyświetl plik

@ -147,8 +147,9 @@ class ObjectId(object):
>>> import datetime
>>> gen_time = datetime.datetime(2010, 1, 1)
>>> early = ObjectId.from_datetime(gen_time)
>>> from activitypub import Manager, ListDatabase
>>> m = Manager(database=ListDatabase())
>>> from activitypub import Manager
>>> from activitypub.database import DummyDatabase
>>> m = Manager(database=DummyDatabase())
>>> n = m.Note(_id=early, attributedTo="alyssa")
>>> m.database.activities.insert_one(n.to_dict())
>>> gen_time = datetime.datetime(2011, 1, 1)

Wyświetl plik

@ -1,113 +1,5 @@
import uuid
import mimetypes
class Manager():
"""
Manager class that ties together ActivityPub objects, defaults,
and a database.
>>> from activitypub import ListDatabase, Manager
>>> db = ListDatabase()
>>> manager = Manager(database=db)
>>>
"""
def __init__(self, context=None, defaults=None, database=None):
self.callback = lambda box, activity_id: None
self.context = context
self.defaults = defaults or self.make_defaults()
self.defaults["$UUID"] = lambda: str(uuid.uuid4())
self.database = database
def make_wrapper(manager, class_):
def wrapper(*args, **kwargs):
return ActivityPubBase.CLASSES[class_](manager, *args, **kwargs)
return wrapper
for class_ in ActivityPubBase.CLASSES:
setattr(self, class_, make_wrapper(self, class_))
def make_defaults(self):
"""
A default field can refer to itself, which means that it needs a
value to begin with.
>>> m = Manager()
>>> n = m.Note(attributedTo="alyssa", id="23")
>>> n.to_dict()
{'@context': 'https://www.w3.org/ns/activitystreams', 'attributedTo': 'alyssa', 'id': 'alyssa/note/23', 'type': 'Note'}
A default can be a $-variable, or the name of a "Class.field_name".
"""
return {
"$SCHEME": "https",
"$HOST": "example.com",
"Person.id": "$SCHEME://$HOST/$id",
"Person.likes": "$id/likes",
"Person.following": "$id/following",
"Person.followers": "$id/followers",
"Person.liked": "$id/liked",
"Person.inbox": "$id/inbox",
"Person.outbox": "$id/outbox",
"Person.url": "$id",
"Note.id": "$attributedTo/note/$id",
}
def from_dict(self, data):
return ActivityPubBase.from_dict(data)
def to_list(self, item):
if isinstance(item, list):
return item
return [item]
def on_post_to_box(self, box, activity):
"""
manager.on_post_to_box("inbox", activity)
"""
self.database.activities.insert_one(
{
"box": box,
"activity": activity.to_dict(),
"type": self.to_list(activity.type),
"remote_id": activity.id,
"meta": {
"undo": False,
"deleted": False
},
}
)
self.callback(box, activity.id)
def delete_reply(self, actor, note):
if note.inReplyTo:
self.database.activities.update_one(
{"activity.object.id": note.inReplyTo},
{"$inc": {"meta.count_reply": -1, "meta.count_direct_reply": -1}},
)
def set_callback(self, callback):
self.callback = callback
def get_followers(self, remote_id):
q = {
"remote_id": remote_id,
"box": "inbox",
"type": "follow",
"meta.undo": False,
}
return [doc["activity"]["actor"]
for doc in self.database.activities.find(q)]
def get_following(self, remote_id):
q = {
"remote_id": remote_id,
"box": "outbox",
"type": "follow",
"meta.undo": False,
}
return [doc["activity"]["object"]
for doc in self.database.activities.find(q)]
class ActivityPubBase():
CLASSES = {}
@ -159,6 +51,7 @@ class ActivityPubBase():
def topological_sort(self, data):
"""
>>> from activitypub import Manager
>>> manager = Manager()
>>> manager.Person(id="alyssa").to_dict()
{'@context': 'https://www.w3.org/ns/activitystreams', 'endpoints': {}, 'followers': 'https://example.com/alyssa/followers', 'following': 'https://example.com/alyssa/following', 'id': 'https://example.com/alyssa', 'inbox': 'https://example.com/alyssa/inbox', 'liked': 'https://example.com/alyssa/liked', 'likes': 'https://example.com/alyssa/likes', 'outbox': 'https://example.com/alyssa/outbox', 'type': 'Person', 'url': 'https://example.com/alyssa'}
@ -182,6 +75,7 @@ class ActivityPubBase():
"""
Parse a string delimited by non-alpha, non-$ symbols.
>>> from activitypub import Manager
>>> m = Manager()
>>> p = m.Person()
>>> p.parse("apple/banana/$variable")
@ -333,6 +227,7 @@ class Organization(Actor):
class Person(Actor):
"""
>>> from activitypub import Manager
>>> m = Manager()
>>> p = m.Person()
>>> p.icon = "image.svg"

Wyświetl plik

@ -0,0 +1,4 @@
from .base import Database, Table
from .dummy import DummyDatabase
from .mongodb import MongoDatabase

Wyświetl plik

@ -0,0 +1,22 @@
class Database():
"""
Base database class.
"""
def __init__(self, Table):
self.activities = Table(self, "activities")
self.u2f = Table(self, "u2f")
self.instances = Table(self, "instances")
self.actors = Table(self, "actors")
self.indieauth = Table(self, "indieauth")
## TODO: put required methods to override here
class Table():
"""
Base table class.
"""
def __init__(self, database=None, name=None):
self.database = database
self.name = name
## TODO: put required methods to override here

Wyświetl plik

@ -2,15 +2,8 @@ import re
import ast
import json
from .bson import ObjectId
class Table():
"""
Base table class.
"""
def __init__(self, database=None, name=None):
self.database = database
self.name = name
from ..bson import ObjectId
from .base import Database, Table
def get_item_in_dict(dictionary, item):
"""
@ -145,7 +138,7 @@ def match(doc, query):
return False
return True
class ListTable(Table):
class DummyTable(Table):
def __init__(self, database=None, name=None, data=None):
super().__init__(database, name)
self.data = data or []
@ -168,14 +161,14 @@ class ListTable(Table):
def sort(self, sort_key, sort_order):
# sort_key = "_id"
# sort_order = 1 or -1
return ListTable(data=sorted(
return DummyTable(data=sorted(
self.data,
key=lambda row: get_item_in_dict(row, sort_key),
reverse=(sort_order == -1)))
def insert_one(self, row):
"""
>>> table = ListTable()
>>> table = DummyTable()
>>> table.count()
0
>>> len(table.data)
@ -206,7 +199,7 @@ class ListTable(Table):
def find(self, query=None, limit=None):
"""
>>> table = ListTable()
>>> table = DummyTable()
>>> table.insert_one({"a": 1, "b": 2})
>>> table.find({"a": 1}) # doctest: +ELLIPSIS
[{'a': 1, 'b': 2, '_id': ObjectId('...')}]
@ -215,17 +208,17 @@ class ListTable(Table):
"""
if query is not None:
if limit is not None:
return ListTable(data=[doc for doc in self.data if match(doc, query)][:limit])
return DummyTable(data=[doc for doc in self.data if match(doc, query)][:limit])
else:
return ListTable(data=[doc for doc in self.data if match(doc, query)])
return DummyTable(data=[doc for doc in self.data if match(doc, query)])
elif limit is not None:
return ListTable(data=self.data[:limit])
return DummyTable(data=self.data[:limit])
else:
return self
def remove(self, query=None):
"""
>>> table = ListTable()
>>> table = DummyTable()
>>> table.insert_one({"a": 1, "b": 2})
>>> table.insert_one({"c": 3, "d": 4})
>>> table.find({"a": 1}) # doctest: +ELLIPSIS
@ -272,14 +265,6 @@ class ListTable(Table):
count_documents = count
class Database():
def __init__(self, Table):
self.activities = Table(self, "activities")
self.u2f = Table(self, "u2f")
self.instances = Table(self, "instances")
self.actors = Table(self, "actors")
self.indieauth = Table(self, "indieauth")
class ListDatabase(Database):
class DummyDatabase(Database):
def __init__(self):
super().__init__(ListTable)
super().__init__(DummyTable)

Wyświetl plik

@ -0,0 +1,30 @@
from pymongo import MongoClient
from .base import Database, Table
class MongoTable(Table):
"""
"""
def __init__(self, database, name):
super().__init__(database, name)
self.collection = getattr(database.DB, name)
def __getattr__(self, attr):
if "collection" in self.__dict__:
return getattr(self.collection, attr)
else:
raise AttributeError("no such attribute: %s" % attr)
def __setattr__(self, attr, value):
if "collection" in self.__dict__ and hasattr(self.collection, attr):
setattr(self.collection, attr, value)
else:
self.__dict__[attr] = value
class MongoDatabase(Database):
def __init__(self, uri, db_name):
self.uri = uri
self.client = MongoClient(self.uri)
self.db_name = db_name
self.DB = self.client[self.db_name]
super().__init__(MongoTable)

Wyświetl plik

@ -0,0 +1,112 @@
import uuid
class Manager():
"""
Manager class that ties together ActivityPub objects, defaults,
and a database.
>>> from activitypub import Manager
>>> from activitypub.database import DummyDatabase
>>> db = DummyDatabase()
>>> manager = Manager(database=db)
>>>
"""
def __init__(self, context=None, defaults=None, database=None):
from .classes import ActivityPubBase
self.callback = lambda box, activity_id: None
self.context = context
self.defaults = defaults or self.make_defaults()
self.defaults["$UUID"] = lambda: str(uuid.uuid4())
self.database = database
def make_wrapper(manager, class_):
def wrapper(*args, **kwargs):
return ActivityPubBase.CLASSES[class_](manager, *args, **kwargs)
return wrapper
for class_ in ActivityPubBase.CLASSES:
setattr(self, class_, make_wrapper(self, class_))
def make_defaults(self):
"""
A default field can refer to itself, which means that it needs a
value to begin with.
>>> m = Manager()
>>> n = m.Note(attributedTo="alyssa", id="23")
>>> n.to_dict()
{'@context': 'https://www.w3.org/ns/activitystreams', 'attributedTo': 'alyssa', 'id': 'alyssa/note/23', 'type': 'Note'}
A default can be a $-variable, or the name of a "Class.field_name".
"""
return {
"$SCHEME": "https",
"$HOST": "example.com",
"Person.id": "$SCHEME://$HOST/$id",
"Person.likes": "$id/likes",
"Person.following": "$id/following",
"Person.followers": "$id/followers",
"Person.liked": "$id/liked",
"Person.inbox": "$id/inbox",
"Person.outbox": "$id/outbox",
"Person.url": "$id",
"Note.id": "$attributedTo/note/$id",
}
def from_dict(self, data):
from .classes import ActivityPubBase
return ActivityPubBase.from_dict(data)
def to_list(self, item):
if isinstance(item, list):
return item
return [item]
def on_post_to_box(self, box, activity):
"""
manager.on_post_to_box("inbox", activity)
"""
self.database.activities.insert_one(
{
"box": box,
"activity": activity.to_dict(),
"type": self.to_list(activity.type),
"remote_id": activity.id,
"meta": {
"undo": False,
"deleted": False
},
}
)
self.callback(box, activity.id)
def delete_reply(self, actor, note):
if note.inReplyTo:
self.database.activities.update_one(
{"activity.object.id": note.inReplyTo},
{"$inc": {"meta.count_reply": -1, "meta.count_direct_reply": -1}},
)
def set_callback(self, callback):
self.callback = callback
def get_followers(self, remote_id):
q = {
"remote_id": remote_id,
"box": "inbox",
"type": "follow",
"meta.undo": False,
}
return [doc["activity"]["actor"]
for doc in self.database.activities.find(q)]
def get_following(self, remote_id):
q = {
"remote_id": remote_id,
"box": "outbox",
"type": "follow",
"meta.undo": False,
}
return [doc["activity"]["object"]
for doc in self.database.activities.find(q)]