piknik

Unnamed repository; edit this file 'description' to name the repository.
Info | Log | Files | Refs | README | LICENSE

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:
Mpiknik/basket.py | 28++++++++++++++++++++++++++++
Apiknik/msg.py | 54++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mpiknik/runnable/mod.py | 2++
Mpiknik/store/__init__.py | 38++++++++++++++++++++++++++++++++++++++
Mrequirements.txt | 1+
Mtests/common.py | 20++++++++++++++++++++
Atests/test_msg.py | 47+++++++++++++++++++++++++++++++++++++++++++++++
Mtests/test_store.py | 6++++++
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()