feedwarrior

Slim, portable tooling for creating and distributing decentralized append logs
git clone git://git.defalsify.org/logwarrior.git
Log | Files | Refs | README | LICENSE

commit 910e029c42f64fb39174d1f3a87557d36a8dee18
parent f5950c1cb6fd7e60e7b192a89a7069cf21d2c83c
Author: lash <dev@holbrook.no>
Date:   Mon,  2 Jan 2023 09:22:34 +0000

Add separate add attachment feature

Diffstat:
DVERSION | 1-
Mfeedwarrior/adapters/fileadapter.py | 55++++++++++++++++++++++++++++++++++++++++++++-----------
Afeedwarrior/cmd/__init__.py | 0
Afeedwarrior/cmd/attach.py | 72++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Afeedwarrior/runnable/main.py | 124+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Mrequirements.txt | 2++
Msetup.py | 15+++++++++++----
7 files changed, 253 insertions(+), 16 deletions(-)

diff --git a/VERSION b/VERSION @@ -1 +0,0 @@ -0.5.1 diff --git a/feedwarrior/adapters/fileadapter.py b/feedwarrior/adapters/fileadapter.py @@ -34,34 +34,67 @@ class fileadapter: self.feeds_uuid = uu - def get(self, uu, **kwargs): + def get_raw_fp(self, fp): + return open(fp, 'r') + + + def get_gz_fp(self, fp): + fp += '.gz' + logg.debug('uncompressing {}'.format(fp)) + return gzip.open(fp, 'rb') + #return open(fp, 'r') + + + def get_with_type(self, uu, **kwargs): entry_path = os.path.join(self.src, 'entries', str(uu)) f = None - if entry_path[len(entry_path)-3:] == '.gz': - logg.debug('uncompressing {}'.format(entry_path)) - f = gzip.open(entry_path, 'rb') - else: - f = open(entry_path, 'r') + typ = 'plain' + if entry_path[-3:] == '.gz': + entry_path = entry_path[:-3] + logg.debug('etnry {}'.format(entry_path)) + try: + f = self.get_raw_fp(entry_path) + except FileNotFoundError: + f = self.get_gz_fp(entry_path) + typ = 'gzip' c = f.read() f.close() - return c + return (c, typ,) + + + def get(self, uu, **kwargs): + r = self.get_with_type(uu, **kwargs) + return r[0] + +# entry_path = os.path.join(self.src, 'entries', str(uu)) +# f = None +# try: +# f = self.get_raw_fp(entry_path) +# except FileNotFoundError: +# f = self.get_gz_fp(entry_path) +# c = f.read() +# f.close() +# return c - def put(self, uu, entry, **kwargs): + def put(self, uu, entry, replace=False, **kwargs): + fm = 'xb' + if replace: + fm = 'wb' entry_serialized = entry.serialize() entry_json = json.dumps(entry_serialized) contents_bytes = entry_json.encode('utf-8') entry_path = os.path.join(self.src, 'entries', str(uu)) - if os.path.exists(entry_path) or os.path.exists(entry_path + '.gz'): + if not replace and (os.path.exists(entry_path) or os.path.exists(entry_path + '.gz')): raise FileExistsError('record {} already exists'.format(str(uu))) f = None if kwargs.get('compress') != None: entry_path += '.gz' - f = gzip.open(entry_path, 'xb') + f = gzip.open(entry_path, fm) else: - f = open(entry_path, 'xb') + f = open(entry_path, fm) f.write(contents_bytes) f.close() diff --git a/feedwarrior/cmd/__init__.py b/feedwarrior/cmd/__init__.py diff --git a/feedwarrior/cmd/attach.py b/feedwarrior/cmd/attach.py @@ -0,0 +1,72 @@ +# standard imports +import sys +import os +from email.message import EmailMessage +from email.mime.multipart import MIMEMultipart +import email +import logging +import uuid +import json +import gzip +import tempfile +import base64 +import uuid + +# external imports +import magic +from mime_parser import parse_mime + +# local imports +import feedwarrior +from feedwarrior import entry as feedentry +from feedwarrior.adapters import fileadapter +from feedwarrior.entry import from_multipart +#from feedwarrior.common import task_ids_to_uuids, check_task_uuids + +logg = logging.getLogger() + + +def parse_args(argparser): + argparser.add_argument('-e', '--entry', type=str, help='entry uuid to modify') + argparser.add_argument('file', type=str, help='file to attach') + return True + + +def check_args(args): + pass + + +def from_file(m, fp): + mg = magic.Magic(flags=magic.MAGIC_MIME_TYPE) + mime_type_str = mg.id_filename(fp) + mime_type = parse_mime(mime_type_str) + logg.info('detected {} type for {}'.format(mime_type, fp)) + + part = EmailMessage() + part.add_header('Content-Type', mime_type_str) + part.add_header('Content-Transfer-Encoding', 'BASE64') + f = open(fp, 'rb') + v = f.read() + f.close() + vb = base64.b64encode(v) + part.set_payload(vb.decode('utf-8')) + + fn = os.path.basename(fp) + part.add_header('Content-Disposition', 'attachment', filename=fn) + + m.attach(part) + return m + + +def execute(config, feed, args): + fa = fileadapter(config.data_dir, None) + (v, fm) = fa.get_with_type(args.entry) #, compress=args.z) + j = json.loads(v) + m = email.message_from_string(j['payload']) + + fp = os.path.realpath(args.file) + attachment = from_file(m, fp) + + entry = from_multipart(attachment) + compress = fm == 'gzip' + fa.put(args.entry, entry, replace=True, compress=compress) diff --git a/feedwarrior/runnable/main.py b/feedwarrior/runnable/main.py @@ -0,0 +1,124 @@ +#!/usr/bin/python + +# Author: Louis Holbrook <dev@holbrook.no> (https://holbrook.no) +# License: GPLv3 +# Description: Work log tool + +# standard imports +import os +import sys +import argparse +import configparser +import json +import logging + +# local imports +import feedwarrior +from feedwarrior.cmd import create as cmd_create +from feedwarrior.cmd import entry as cmd_entry +from feedwarrior.cmd import show as cmd_show +from feedwarrior.cmd import ls as cmd_list +from feedwarrior.cmd import add as cmd_add +from feedwarrior.cmd import attach as cmd_attach + +logging.basicConfig(level=logging.ERROR) +logg = logging.getLogger() + + +def matches_part(full, part): + if len(part) > len(full): + return False + return full[:len(part)] == part + + + +argparser = argparse.ArgumentParser(description='create and manipulate feedwarrior feeds') +argparser.add_argument('-l', help='feed log to operate on') +argparser.add_argument('-c', type=str, default='config.ini', help='configuration file') +argparser.add_argument('-v', action='store_true', help='be verbose') +argparser.add_argument('--headers', action='store_true', help='add headers in output') +sub = argparser.add_subparsers() +# TODO: add subparser to same level flags as main parser +sub.dest = 'command' +sub_entry = sub.add_parser('entry', help='add entry to feed') +cmd_entry.parse_args(sub_entry) +sub_show = sub.add_parser('show', help='view feed log') +cmd_show.parse_args(sub_show) +sub_create = sub.add_parser('create', help='create new feed') +cmd_create.parse_args(sub_create) +sub_list = sub.add_parser('list', help='list feeds') +cmd_list.parse_args(sub_list) +sub_add = sub.add_parser('add', help='add new entry with editor') +cmd_add.parse_args(sub_add) +sub_attach = sub.add_parser('attach', help='attach file to existing entry') +cmd_attach.parse_args(sub_attach) + + +args = argparser.parse_known_args() +args = argparser.parse_args(args[1], args[0]) +if args.v: + logging.getLogger().setLevel(logging.DEBUG) + +logg.debug('attempting to load config {}'.format(args.c)) +config = feedwarrior.load_config(args.c) + + +def get_feed_by_name(s): + index_path = os.path.join(config.feeds_dir, 'names', s) + resolved_path = os.path.realpath(index_path) + os.stat(resolved_path) + logg.debug('feed path {} resolves to {}'.format(index_path, resolved_path)) + return os.path.basename(resolved_path) + + + +def main(): + feed_current = None + if args.l != None: + try: + uu = feedwarrior.common.parse_uuid(args.l) + feed_current = feedwarrior.feed(uu) + except ValueError: + try: + uu = get_feed_by_name(args.l) + feed_current = feedwarrior.feed(uu) + except FileNotFoundError as e: + sys.stderr.write('cannot resolve feed {}\n'.format(args.l)) + sys.exit(1) + + cmd_mod = None + if args.command == None or matches_part('show', args.command): + if feed_current == None: + sys.stderr.write('plesae speficy a feed for showing\n') + sys.exit(1) + feed_current = feedwarrior.load_feed(config.data_dir, feed_current.uuid) + cmd_mod = cmd_show + elif matches_part('create', args.command): + feed_current = feedwarrior.feed(parent=feed_current) + cmd_mod = cmd_create + elif matches_part('entry', args.command): + cmd_mod = cmd_entry + elif matches_part('list', args.command) or args.command == 'ls': + cmd_mod = cmd_list + elif matches_part('add', args.command): + cmd_mod = cmd_add + elif matches_part('attach', args.command): + cmd_mod = cmd_attach + else: + sys.stderr.write('invalid command {}\n'.format(args.command)) + sys.exit(1) + + try: + os.makedirs(config.entries_dir, mode=0o777, exist_ok=False) + os.makedirs(config.feeds_dir, mode=0o777, exist_ok=False) + os.makedirs(os.path.join(config.feeds_dir, 'names'), mode=0o777, exist_ok=False) + logg.debug('creating datadir {}'.format(config.data_dir)) + except FileExistsError as e: + logg.debug('found existing datadir {}'.format(config.data_dir)) + + cmd_mod.check_args(args) + cmd_mod.execute(config, feed_current, args) + + +if __name__ == '__main__': + main() diff --git a/requirements.txt b/requirements.txt @@ -1,2 +1,4 @@ pyxdg~=0.26 tasklib>=1.3.0,<2.0.0 +filemagic~=1.6 +mime_parser~=1.2.0 diff --git a/setup.py b/setup.py @@ -20,12 +20,19 @@ setup( packages=[ 'feedwarrior', 'feedwarrior.cmd', - 'feedwarrior.adapters' + 'feedwarrior.adapters', + 'feedwarrior.runnable', ], install_requires=[ requirements, ], - scripts = [ - 'scripts/feedwarrior', - ] + #scripts = [ + # 'scripts/feedwarrior', + # ], + entry_points = { + 'console_scripts': [ + 'feedwarrior = feedwarrior.runnable.main:main', + ], + }, + license_files = ('LICENSE',), )