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