We can rebuild it without ORM. We have the technology.

This commit is contained in:
RunasSudo 2017-09-22 15:59:15 +10:00
parent ba26fc38f8
commit afd7f59389
Signed by: RunasSudo
GPG Key ID: 7234E476BF21C61A
8 changed files with 309 additions and 60 deletions

0
eos/base/__init__.py Normal file
View File

45
eos/base/election.py Normal file
View File

@ -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 <http://www.gnu.org/licenses/>.
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)

68
eos/base/tests.py Normal file
View File

@ -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 <http://www.gnu.org/licenses/>.
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)

114
eos/base/workflow.py Normal file
View File

@ -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 <http://www.gnu.org/licenses/>.
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))

View File

@ -75,7 +75,6 @@ class BigInt(EosObject):
('__ge__', lambda x: x >= 0) ('__ge__', lambda x: x >= 0)
]: ]:
def make_operator_func(func_): def make_operator_func(func_):
# Create a closure
def operator_func(other): def operator_func(other):
if not isinstance(other, BigInt): if not isinstance(other, BigInt):
other = BigInt(other) other = BigInt(other)

View File

@ -14,61 +14,95 @@
# You should have received a copy of the GNU Affero General Public License # You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>. # along with this program. If not, see <http://www.gnu.org/licenses/>.
import mongoengine import pymongo
import uuid
# Database
# ========
client = pymongo.MongoClient()
db = client['test']
# Fields
# ======
class Field: class Field:
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
if 'default' in kwargs: self.default = kwargs.get('default', None)
self.required = False self.hashed = kwargs.get('hashed', True)
self.default = kwargs['default']
else:
self.required = True
self.default = None
class PrimitiveField(Field): class PrimitiveField(Field):
def to_python(self): def serialise(self, value):
return self.mongo_field( return value
required=self.required,
default=self.default def deserialise(self, value):
) return value
class StringField(PrimitiveField): DictField = PrimitiveField
mongo_field = mongoengine.StringField 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): 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 # Process fields
fields = {} fields = cls._fields if hasattr(cls, '_fields') else {}
for attr, val in attrs.items(): for attr in list(dir(cls)):
val = getattr(cls, attr)
if isinstance(val, Field): if isinstance(val, Field):
fields[attr] = val fields[attr] = val
attrs[attr] = val.to_python() delattr(cls, attr)
attrs['_fields'] = fields cls._fields = fields
return meta, name, bases, attrs cls._name = cls.__module__ + '.' + cls.__qualname__
return cls
class TopLevelObjectType(mongoengine.base.TopLevelDocumentMetaclass, EosObjectType): class EosObject(metaclass=EosObjectType):
def __new__(meta, name, bases, attrs): def __init__(self, *args, **kwargs):
meta, name, bases, attrs = meta.before_new(meta, name, bases, attrs) for attr, val in self._fields.items():
return super().__new__(meta, name, bases, attrs) setattr(self, attr, kwargs.get(attr, val.default))
class EmbeddedObjectType(mongoengine.base.DocumentMetaclass, EosObjectType): TopLevelObject = EosObject
def __new__(meta, name, bases, attrs): EmbeddedObject = EosObject
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
}

View File

@ -36,18 +36,3 @@ class PyTestCase(TestCase):
self.assertEqual(person2.address, 'Address 2') self.assertEqual(person2.address, 'Address 2')
self.assertEqual(person1.say_hi(), 'Hello! My name is John') self.assertEqual(person1.say_hi(), 'Hello! My name is John')
self.assertEqual(person2.say_hi(), 'Hello! My name is James') 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')

4
requirements.txt Normal file
View File

@ -0,0 +1,4 @@
mypy==0.521
pymongo==3.5.1
Transcrypt==3.6.50
typed-ast==1.0.4