commit f7da876d3a1d6349e7b3f8f2980d6fe69ee27caa
parent 3cd29258719cc43745cc9d3a9d00699c0969207f
Author: lash <dev@holbrook.no>
Date: Tue, 8 Nov 2022 12:57:21 +0000
Add message store, get and put for text
Diffstat:
8 files changed, 196 insertions(+), 0 deletions(-)
diff --git a/piknik/basket.py b/piknik/basket.py
@@ -1,9 +1,15 @@
+# standard imports
+import logging
+
# external imports
import shep
# local imports
from .error import DeadIssue
from .issue import Issue
+from .msg import IssueMessage
+
+logg = logging.getLogger(__name__)
class Basket:
@@ -25,6 +31,8 @@ class Basket:
self.__tags = state_factory.create_tags()
self.__tags.sync(ignore_auto=False)
+ self.__msg = state_factory.create_messages()
+
self.issues_rev = {}
@@ -130,3 +138,23 @@ class Basket:
if r == 'UNTAGGED':
r = '(' + r + ')'
return shep.state.split_elements(r)
+
+
+ def __get_msg(self, issue_id):
+ try:
+ v = self.__msg.get(issue_id)
+ return IssueMessage.from_string(v)
+ except FileNotFoundError:
+ logg.debug('instantiating new message log for {}'.format(issue_id))
+
+ v = self.state.get(issue_id)
+ o = Issue.from_str(v)
+ return IssueMessage(o)
+
+
+ def msg(self, issue_id, *args):
+ m = self.__get_msg(issue_id)
+ m.add(*args)
+ ms = m.as_bytes()
+ self.__msg.put(issue_id, ms)
+ return m
diff --git a/piknik/msg.py b/piknik/msg.py
@@ -0,0 +1,54 @@
+# standard imports
+import logging
+import uuid
+
+#from email.message import EmailMessage as Message
+from email.message import Message
+from email import message_from_string
+from email.policy import Compat32
+
+
+logg = logging.getLogger(__name__)
+
+
+class IssueMessage(Message):
+
+ def __init__(self, issue):
+ super(IssueMessage, self).__init__()
+ self.add_header('Subject', issue.title)
+ self.add_header('X-Piknik-Id', issue.id)
+ self.set_payload(None)
+ self.set_type('multipart/mixed')
+ self.set_boundary(str(uuid.uuid4()))
+
+
+ @staticmethod
+ def from_string(self, v):
+ return message_from_string(v)
+
+
+ def add_text(self, m, v):
+ p = Message()
+ p.add_header('Content-Transfer-Encoding', 'QUOTED-PRINTABLE')
+ p.set_charset('UTF-8')
+ p.set_payload(str(v))
+ m.attach(p)
+
+
+ def add(self, *args, related_id=None):
+ m_id = uuid.uuid4()
+ m = Message()
+ m.add_header('X-Piknik-Msg-Id', str(m_id))
+ if related_id != None:
+ m.add_header('In-Reply-To', related_id)
+ m.set_payload(None)
+ m.set_type('multipart/mixed')
+ m.set_boundary(str(uuid.uuid4()))
+ for a in args:
+ p = a[:2]
+ v = a[2:]
+ if p == 'f:':
+ self.add_file(m, v)
+ elif p == 's:':
+ self.add_text(m, v)
+ self.attach(m)
diff --git a/piknik/runnable/mod.py b/piknik/runnable/mod.py
@@ -14,6 +14,8 @@ argp.add_argument('--finish', action='store_true', help='Set issue as finished (
argp.add_argument('-s', '--state', type=str, help='Move to state')
argp.add_argument('-t', '--tag', type=str, action='append', default=[], help='Add tag to issue')
argp.add_argument('-u', '--untag', type=str, action='append', default=[], help='Remove tag from issue')
+argp.add_argument('-f', '--file', type=str, action='append', help='Add message file part')
+argp.add_argument('-m', '--message', type=str, action='append', default=[], help='Add message text part')
argp.add_argument('issue_id', type=str, help='Issue id to modify')
arg = argp.parse_args(sys.argv[1:])
diff --git a/piknik/store/__init__.py b/piknik/store/__init__.py
@@ -1,7 +1,40 @@
import os
+# external imports
from shep.store.file import SimpleFileStoreFactory
from shep.persist import PersistedState
+from leveldir.hex import HexDir
+
+
+def default_formatter(hx):
+ return hx.lower()
+
+
+class MsgDir(HexDir):
+
+ def __init__(self, root_path):
+ super(MsgDir, self).__init__(root_path, 36, levels=2, prefix_length=0, formatter=default_formatter)
+
+
+ def __check(self, key, content, prefix):
+ pass
+
+
+ def get(self, k):
+ fp = self.to_filepath(k)
+ f = None
+ f = open(fp, 'r')
+ r = f.read()
+ f.close()
+ return r
+
+
+ def key_to_string(self, k):
+ return k.decode('utf-8')
+
+
+ def put(self, k, v):
+ return self.add(k.encode('utf-8'), v)
class FileStoreFactory:
@@ -36,3 +69,8 @@ class FileStoreFactory:
state.alias(v, s)
return state
+
+
+ def create_messages(self):
+ d = os.path.join(self.directory, '.msg')
+ return MsgDir(d)
diff --git a/requirements.txt b/requirements.txt
@@ -1 +1,2 @@
shep~=0.2.11
+leveldir~=0.3.1
diff --git a/tests/common.py b/tests/common.py
@@ -11,6 +11,22 @@ def debug_out(self, k, v):
logg.debug('TRACE: {} {}'.format(k, v))
+class TestMsgStore:
+
+ def __init__(self):
+ self.store = {}
+
+ def put(self, k, v):
+ self.store[k] = v
+
+ def get(self, k):
+ try:
+ return self.store[k]
+ except KeyError:
+ pass
+ raise FileNotFoundError(k)
+
+
class TestStates:
def create_states(*args, **kwargs):
@@ -19,3 +35,7 @@ class TestStates:
def create_tags(*args, **kwargs):
return shep.State(0, *args, event_callback=debug_out, check_alias=False, **kwargs)
+
+
+ def create_messages(*args):
+ return TestMsgStore()
diff --git a/tests/test_msg.py b/tests/test_msg.py
@@ -0,0 +1,47 @@
+# standard imports
+import unittest
+import logging
+import json
+
+# local imports
+from piknik import (
+ Basket,
+ Issue,
+ )
+from piknik.msg import IssueMessage
+
+# test imports
+from tests.common import TestStates
+
+logging.basicConfig(level=logging.DEBUG)
+logg = logging.getLogger()
+
+
+
+class TestMsg(unittest.TestCase):
+
+ def setUp(self):
+ self.b = Basket(TestStates())
+
+
+ def test_basic(self):
+ o = Issue('foo')
+ v = IssueMessage(o)
+ self.assertEqual(v.get('Subject'), 'foo')
+
+
+ def test_single_content(self):
+ o = Issue('foo')
+ v = self.b.add(o)
+ m = self.b.msg(v, 's:foo')
+
+
+ def test_multi_content(self):
+ o = Issue('foo')
+ v = self.b.add(o)
+ m = self.b.msg(v, 's:foo', 's:bar', 's:baz')
+ print(m)
+
+
+if __name__ == '__main__':
+ unittest.main()
diff --git a/tests/test_store.py b/tests/test_store.py
@@ -78,5 +78,11 @@ class TestStore(unittest.TestCase):
self.assertIn('PINKY', r)
+ def test_msg_putget(self):
+ o = Issue('foo')
+ issue_id = self.b.add(o)
+ m = self.b.msg(issue_id, 'bar')
+
+
if __name__ == '__main__':
unittest.main()