We can rebuild it without ORM. We have the technology.
This commit is contained in:
parent
ba26fc38f8
commit
afd7f59389
0
eos/base/__init__.py
Normal file
0
eos/base/__init__.py
Normal file
45
eos/base/election.py
Normal file
45
eos/base/election.py
Normal 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
68
eos/base/tests.py
Normal 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
114
eos/base/workflow.py
Normal 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))
|
@ -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)
|
||||||
|
@ -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
|
|
||||||
}
|
|
||||||
|
@ -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
4
requirements.txt
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
mypy==0.521
|
||||||
|
pymongo==3.5.1
|
||||||
|
Transcrypt==3.6.50
|
||||||
|
typed-ast==1.0.4
|
Loading…
Reference in New Issue
Block a user