From afd7f593891fabf27d05113a62bcb8354ed65522 Mon Sep 17 00:00:00 2001 From: RunasSudo Date: Fri, 22 Sep 2017 15:59:15 +1000 Subject: [PATCH] We can rebuild it without ORM. We have the technology. --- eos/base/__init__.py | 0 eos/base/election.py | 45 ++++++++++++++ eos/base/tests.py | 68 +++++++++++++++++++++ eos/base/workflow.py | 114 ++++++++++++++++++++++++++++++++++ eos/core/bigint/js.py | 1 - eos/core/objects/python.py | 122 ++++++++++++++++++++++++------------- eos/core/tests.py | 15 ----- requirements.txt | 4 ++ 8 files changed, 309 insertions(+), 60 deletions(-) create mode 100644 eos/base/__init__.py create mode 100644 eos/base/election.py create mode 100644 eos/base/tests.py create mode 100644 eos/base/workflow.py create mode 100644 requirements.txt diff --git a/eos/base/__init__.py b/eos/base/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/eos/base/election.py b/eos/base/election.py new file mode 100644 index 0000000..fa520bd --- /dev/null +++ b/eos/base/election.py @@ -0,0 +1,45 @@ +# Eos - Verifiable elections +# Copyright © 2017 RunasSudo (Yingtong Li) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from eos.core.objects import * +from eos.base.workflow import * + +class BallotQuestion(EmbeddedObject): + pass + +class PlaintextBallotQuestion(BallotQuestion): + choices = ListField(IntField()) + +class Ballot(EmbeddedObject): + _id = UUIDField() + questions = EmbeddedObjectListField(BallotQuestion) + +class Voter(EmbeddedObject): + _id = UUIDField() + ballots = EmbeddedObjectListField(Ballot) + +class Question(EmbeddedObject): + prompt = StringField() + +class ApprovalQuestion(Question): + choices = ListField(StringField()) + +class Election(TopLevelObject): + _id = UUIDField() + workflow = EmbeddedObjectField(Workflow) + name = StringField() + voters = EmbeddedObjectListField(Voter, hashed=False) + questions = EmbeddedObjectListField(Question) diff --git a/eos/base/tests.py b/eos/base/tests.py new file mode 100644 index 0000000..6d443e6 --- /dev/null +++ b/eos/base/tests.py @@ -0,0 +1,68 @@ +# Eos - Verifiable elections +# Copyright © 2017 RunasSudo (Yingtong Li) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from unittest import TestCase + +from eos.base.election import * +from eos.base.workflow import * +from eos.core.objects import * + +class ElectionTestCase(TestCase): + @classmethod + def setUpClass(cls): + client.drop_database('test') + + def test_run_election(self): + # Set up election + election = Election() + election.workflow = WorkflowBase(election) + + self.assertEqual(election.workflow.get_task('eos.base.workflow.TaskConfigureElection').status, WorkflowTask.Status.READY) + + election.name = 'Test Election' + + for i in range(3): + election.voters.append(Voter()) + + question = ApprovalQuestion() + question.prompt = 'President' + question.choices.append('John Smith') + question.choices.append('Joe Bloggs') + question.choices.append('John Q. Public') + election.questions.append(question) + + question = ApprovalQuestion() + question.prompt = 'Chairman' + question.choices.append('John Doe') + question.choices.append('Andrew Citizen') + election.questions.append(question) + + #election.save() + + # Check that it saved + #self.assertEqual(Election.objects.get(0)._id, election._id) # TODO: Compare JSON + + # Retrieve from scratch, too + #self.db.close() + #self.db = mongoengine.connect('test') + #self.assertEqual(Election.objects.get(0)._id, election._id) + + # Freeze election + self.assertEqual(election.workflow.get_task('eos.base.workflow.TaskConfigureElection').status, WorkflowTask.Status.READY) + self.assertEqual(election.workflow.get_task('eos.base.workflow.TaskOpenVoting').status, WorkflowTask.Status.NOT_READY) + election.workflow.get_task('eos.base.workflow.TaskConfigureElection').exit() + self.assertEqual(election.workflow.get_task('eos.base.workflow.TaskConfigureElection').status, WorkflowTask.Status.EXITED) + self.assertEqual(election.workflow.get_task('eos.base.workflow.TaskOpenVoting').status, WorkflowTask.Status.READY) diff --git a/eos/base/workflow.py b/eos/base/workflow.py new file mode 100644 index 0000000..d89cea2 --- /dev/null +++ b/eos/base/workflow.py @@ -0,0 +1,114 @@ +# Eos - Verifiable elections +# Copyright © 2017 RunasSudo (Yingtong Li) +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . + +from eos.core.objects import * + +class WorkflowTask(EmbeddedObject): + class Status: + NOT_READY = 10 + READY = 20 + #ENTERED = 30 + #COMPLETE = 40 + EXITED = 50 + + depends_on = [] + provides = [] + + status = IntField() + + def __init__(self, workflow=None, *args, **kwargs): + super().__init__(*args, **kwargs) + + self.workflow = workflow + + if self.workflow is None: + self.workflow = self._instance + + self.status = WorkflowTask.Status.READY if self.are_dependencies_met() else WorkflowTask.Status.NOT_READY + + self.listeners = { + 'exit': [] + } + + # Helpers + + def on_dependency_exit(): + self.status = WorkflowTask.Status.READY if self.are_dependencies_met() else WorkflowTask.Status.NOT_READY + for depends_on_desc in self.depends_on: + for depends_on_task in self.workflow.get_tasks(depends_on_desc): + depends_on_task.listeners['exit'].append(on_dependency_exit) + + def are_dependencies_met(self): + for depends_on_desc in self.depends_on: + for depends_on_task in self.workflow.get_tasks(depends_on_desc): + if depends_on_task.status is not WorkflowTask.Status.EXITED: + return False + return True + + @classmethod + def satisfies(cls, descriptor): + return cls._name == descriptor or descriptor in cls.provides + + def fire_event(self, event): + for listener in self.listeners[event]: + listener() + + def exit(self): + if self.status is not WorkflowTask.Status.READY: + raise Exception() + + self.status = WorkflowTask.Status.EXITED + self.fire_event('exit') + +class Workflow(EmbeddedObject): + tasks = EmbeddedObjectListField(WorkflowTask) + meta = { + 'abstract': True + } + + def __init__(self, election=None, *args, **kwargs): + super().__init__(*args, **kwargs) + self.election = election if election else self._instance + + def get_tasks(self, descriptor): + yield from (task for task in self.tasks if task.satisfies(descriptor)) + + def get_task(self, descriptor): + try: + return next(self.get_tasks(descriptor)) + except StopIteration: + return None + +# Concrete tasks +# ============== + +class TaskConfigureElection(WorkflowTask): + #def on_enter(self): + # self.status = WorkflowTask.Status.COMPLETE + pass + +class TaskOpenVoting(WorkflowTask): + depends_on = ['eos.base.workflow.TaskConfigureElection'] + +# Concrete workflows +# ================== + +class WorkflowBase(Workflow): + def __init__(self, election=None, *args, **kwargs): + super().__init__(election, *args, **kwargs) + + self.tasks.append(TaskConfigureElection(self)) + self.tasks.append(TaskOpenVoting(self)) diff --git a/eos/core/bigint/js.py b/eos/core/bigint/js.py index 522fcee..9d04477 100644 --- a/eos/core/bigint/js.py +++ b/eos/core/bigint/js.py @@ -75,7 +75,6 @@ class BigInt(EosObject): ('__ge__', lambda x: x >= 0) ]: def make_operator_func(func_): - # Create a closure def operator_func(other): if not isinstance(other, BigInt): other = BigInt(other) diff --git a/eos/core/objects/python.py b/eos/core/objects/python.py index 6325dfe..49cf97c 100644 --- a/eos/core/objects/python.py +++ b/eos/core/objects/python.py @@ -14,61 +14,95 @@ # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see . -import mongoengine +import pymongo + +import uuid + +# Database +# ======== + +client = pymongo.MongoClient() +db = client['test'] + +# Fields +# ====== class Field: def __init__(self, *args, **kwargs): - if 'default' in kwargs: - self.required = False - self.default = kwargs['default'] - else: - self.required = True - self.default = None + self.default = kwargs.get('default', None) + self.hashed = kwargs.get('hashed', True) class PrimitiveField(Field): - def to_python(self): - return self.mongo_field( - required=self.required, - default=self.default - ) + def serialise(self, value): + return value + + def deserialise(self, value): + return value -class StringField(PrimitiveField): - mongo_field = mongoengine.StringField +DictField = PrimitiveField +IntField = PrimitiveField +ListField = PrimitiveField +StringField = PrimitiveField + +class EmbeddedObjectField(Field): + def __init__(self, object_type=None, *args, **kwargs): + super().__init__(*args, **kwargs) + self.object_type = object_type + + def serialise(self, value): + return value.serialise_and_wrap(self.object_type) + + def deserialise(self, value): + return EosObject.deserialise_and_unwrap(value, self.object_type) + +class ListField(Field): + def __init__(self, element_field=None, *args, **kwargs): + super().__init__(default=[], *args, **kwargs) + self.element_field = element_field + + def serialise(self, value): + return [self.element_field.serialise(x) for x in value] + + def deserialise(self, value): + return [self.element_field.deserialise(x) for x in value] + +EmbeddedObjectListField = ListField + +class UUIDField(Field): + def __init__(self, *args, **kwargs): + super().__init__(default=uuid.uuid4, *args, **kwargs) + + def serialise(self, value): + return str(uuid.uuid4) + + def unserialise(self, value): + return uuid.uuid4(value) + +# Objects +# ======= class EosObjectType(type): - def before_new(meta, name, bases, attrs): + def __new__(meta, name, bases, attrs): + #meta, name, bases, attrs = meta.before_new(meta, name, bases, attrs) + cls = type.__new__(meta, name, bases, attrs) + # Process fields - fields = {} - for attr, val in attrs.items(): + fields = cls._fields if hasattr(cls, '_fields') else {} + for attr in list(dir(cls)): + val = getattr(cls, attr) if isinstance(val, Field): fields[attr] = val - attrs[attr] = val.to_python() - attrs['_fields'] = fields + delattr(cls, attr) + cls._fields = fields - return meta, name, bases, attrs + cls._name = cls.__module__ + '.' + cls.__qualname__ + + return cls -class TopLevelObjectType(mongoengine.base.TopLevelDocumentMetaclass, EosObjectType): - def __new__(meta, name, bases, attrs): - meta, name, bases, attrs = meta.before_new(meta, name, bases, attrs) - return super().__new__(meta, name, bases, attrs) +class EosObject(metaclass=EosObjectType): + def __init__(self, *args, **kwargs): + for attr, val in self._fields.items(): + setattr(self, attr, kwargs.get(attr, val.default)) -class EmbeddedObjectType(mongoengine.base.DocumentMetaclass, EosObjectType): - def __new__(meta, name, bases, attrs): - meta, name, bases, attrs = meta.before_new(meta, name, bases, attrs) - return super().__new__(meta, name, bases, attrs) - -class EosObject(): - pass - -class DocumentObject(EosObject): - pass - -class TopLevelObject(DocumentObject, mongoengine.Document, metaclass=TopLevelObjectType): - meta = { - 'abstract': True - } - -class EmbeddedObject(DocumentObject, mongoengine.EmbeddedDocument, metaclass=EmbeddedObjectType): - meta = { - 'abstract': True - } +TopLevelObject = EosObject +EmbeddedObject = EosObject diff --git a/eos/core/tests.py b/eos/core/tests.py index 274332d..14a6f4a 100644 --- a/eos/core/tests.py +++ b/eos/core/tests.py @@ -36,18 +36,3 @@ class PyTestCase(TestCase): self.assertEqual(person2.address, 'Address 2') self.assertEqual(person1.say_hi(), 'Hello! My name is John') self.assertEqual(person2.say_hi(), 'Hello! My name is James') - - #def test_default_py(self): - # try: - # person1 = self.Person() # No name - # self.fail() - # except KeyError: - # pass - # - # try: - # person1 = self.Person(address='Address') # No name - # self.fail() - # except KeyError: - # pass - # - # person1 = self.Person(name='John') diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..675e387 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,4 @@ +mypy==0.521 +pymongo==3.5.1 +Transcrypt==3.6.50 +typed-ast==1.0.4