From 851af921fae0e0bcc090de778354fc08d927380c Mon Sep 17 00:00:00 2001 From: Douglas Blank Date: Thu, 19 Jul 2018 09:45:11 -0400 Subject: [PATCH] Refactor database; added MongoDatabase --- README.md | 3 +- activitypub/__init__.py | 4 +- activitypub/bson/objectid.py | 5 +- activitypub/classes.py | 111 +---------------- activitypub/database/__init__.py | 4 + activitypub/database/base.py | 22 ++++ .../{database.py => database/dummy.py} | 39 ++---- activitypub/database/mongodb.py | 30 +++++ activitypub/manager.py | 112 ++++++++++++++++++ 9 files changed, 190 insertions(+), 140 deletions(-) create mode 100644 activitypub/database/__init__.py create mode 100644 activitypub/database/base.py rename activitypub/{database.py => database/dummy.py} (88%) create mode 100644 activitypub/database/mongodb.py create mode 100644 activitypub/manager.py diff --git a/README.md b/README.md index 6503713..5523a1b 100644 --- a/README.md +++ b/README.md @@ -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") diff --git a/activitypub/__init__.py b/activitypub/__init__.py index 86b3b47..3f1df00 100644 --- a/activitypub/__init__.py +++ b/activitypub/__init__.py @@ -1,2 +1,2 @@ -from .classes import Manager -from .database import Database, ListDatabase +from .manager import Manager +from .classes import * diff --git a/activitypub/bson/objectid.py b/activitypub/bson/objectid.py index b0c8811..4be91c3 100644 --- a/activitypub/bson/objectid.py +++ b/activitypub/bson/objectid.py @@ -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) diff --git a/activitypub/classes.py b/activitypub/classes.py index 60d6b81..1616c95 100644 --- a/activitypub/classes.py +++ b/activitypub/classes.py @@ -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" diff --git a/activitypub/database/__init__.py b/activitypub/database/__init__.py new file mode 100644 index 0000000..23e7b39 --- /dev/null +++ b/activitypub/database/__init__.py @@ -0,0 +1,4 @@ + +from .base import Database, Table +from .dummy import DummyDatabase +from .mongodb import MongoDatabase diff --git a/activitypub/database/base.py b/activitypub/database/base.py new file mode 100644 index 0000000..a83e500 --- /dev/null +++ b/activitypub/database/base.py @@ -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 diff --git a/activitypub/database.py b/activitypub/database/dummy.py similarity index 88% rename from activitypub/database.py rename to activitypub/database/dummy.py index 21b2ce1..0f1985e 100644 --- a/activitypub/database.py +++ b/activitypub/database/dummy.py @@ -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) diff --git a/activitypub/database/mongodb.py b/activitypub/database/mongodb.py new file mode 100644 index 0000000..c5f98b5 --- /dev/null +++ b/activitypub/database/mongodb.py @@ -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) diff --git a/activitypub/manager.py b/activitypub/manager.py new file mode 100644 index 0000000..15323d2 --- /dev/null +++ b/activitypub/manager.py @@ -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)] +