~mapperr/trl

f9a6cd5ee6afd98e66b2adee799ee8be0ae18b11 — mapperr 2 years ago 36d1ec2
Refactor and tests
M .gitignore => .gitignore +1 -1
@@ 40,7 40,7 @@ MANIFEST
pip-log.txt
pip-delete-this-directory.txt

# Unit test / coverage reports
# Unit tests / coverage reports
htmlcov/
.tox/
.nox/

A tests/__init__.py => tests/__init__.py +0 -0
A tests/unit/__init__.py => tests/unit/__init__.py +0 -0
A tests/unit/test_shortener.py => tests/unit/test_shortener.py +106 -0
@@ 0,0 1,106 @@
import unittest
from typing import List
from unittest import mock
from unittest.mock import Mock

from trullo.shortcuttable import Shortcuttable
from trullo.shortener import Shortener
from trullo.trl_board import TrlBoard
from trullo.trl_card import TrlCard
from trullo.trl_list import TrlList


class TestShortener(unittest.TestCase):
    def test_matches(self):
        sh = Shortener
        start_match = sh.is_a_match('pil', 'pillow')
        self.assertTrue(start_match)
        middle_match = sh.is_a_match('grat', 'integration')
        self.assertTrue(middle_match)
        end_match = sh.is_a_match('ver', 'forever')
        self.assertTrue(end_match)

    def test_wrong_matches(self):
        sh = Shortener
        match = sh.is_a_match('spil', 'pillow')
        self.assertFalse(match)

    def test_matches_in_list(self):
        short1: Shortcuttable = \
            mock.create_autospec(Shortcuttable, spec_set=True)
        short1.get_normalized_name = Mock(return_value='qwerty')
        short2: Shortcuttable = \
            mock.create_autospec(Shortcuttable, spec_set=True)
        short2.get_normalized_name = Mock(return_value='asdfgh')
        short3: Shortcuttable = \
            mock.create_autospec(Shortcuttable, spec_set=True)
        short3.get_normalized_name = Mock(return_value='qwertyasdfgh')
        short4: Shortcuttable = \
            mock.create_autospec(Shortcuttable, spec_set=True)
        short4.get_normalized_name = Mock(return_value='')

        sh = Shortener
        shorties = [short1, short2]
        matches: List = sh.get_matches('er', shorties)
        self.assertEqual(1, len(matches))
        matches: List = sh.get_matches('df', shorties)
        self.assertEqual(1, len(matches))
        matches: List = sh.get_matches('mn', shorties)
        self.assertEqual(0, len(matches))
        matches: List = sh.get_matches('rty', shorties + [short3])
        self.assertEqual(2, len(matches))
        matches: List = sh.get_matches('er', shorties + [short4])
        self.assertEqual(1, len(matches))

    def test_normalization(self):
        card1 = TrlCard('idc1', shortLink := 'qWeRt1',
                        {'name': 'Design the project in 2 days',
                         'shortLink': shortLink})
        card2 = TrlCard('idc2', shortLink := 'AsD3gH',
                        {'name': 'Produce a non-techical doc, clear and easy',
                         'shortLink': shortLink})
        card3 = TrlCard('idc3', shortLink := 'zXcvb7',
                        {'name': ' Implement a trim() function',
                         'shortLink': shortLink})
        list1 = TrlList('idl1', {'name': 'To Do'})
        board1 = TrlBoard('idb1', shortLink := 'p01UyT', [list1],
                          [card1, card2, card3],
                          {'name': 'my Super Board',
                           'shortLink': shortLink})

        card1_n = card1.get_normalized_name()
        self.assertNotIn('D', card1_n)
        self.assertNotIn(' ', card1_n)
        self.assertIn('2', card1_n)
        self.assertIn('project', card1_n)
        self.assertIn('design', card1_n)
        self.assertIn('days', card1_n)

        card2_n = card2.get_normalized_name()
        self.assertNotIn('P', card2_n)
        self.assertNotIn(' ', card2_n)
        self.assertNotIn('-', card2_n)
        self.assertNotIn(',', card2_n)
        self.assertIn('nontech', card2_n)
        self.assertIn('docclear', card2_n)
        self.assertIn('produce', card2_n)

        card3_n = card3.get_normalized_name()
        self.assertNotIn('I', card3_n)
        self.assertNotIn(' ', card2_n)
        self.assertNotIn(' impl', card3_n)
        self.assertNotIn('(', card3_n)
        self.assertIn('trimfunc', card3_n)

        list1_n = list1.get_normalized_name()
        self.assertNotIn('T', list1_n)
        self.assertNotIn(' ', list1_n)
        self.assertIn('idl1', list1_n)
        self.assertIn('todo', list1_n)

        board1_n = board1.get_normalized_name()
        self.assertNotIn(' ', board1_n)
        self.assertNotIn('U', board1_n)
        self.assertIn('mysuper', board1_n)
        self.assertIn('p01u', board1_n)


M trullo.py => trullo.py +19 -123
@@ 46,55 46,23 @@ env:

"""
import logging
import os
import pprint
import subprocess
import tempfile
import urllib
from typing import List

from docopt import docopt

from trullo.printer import Printer
from trullo.shortener import Shortener
from trullo.tclient import TClient
from trullo.trl_board import TrlBoard
from trullo.trl_card import TrlCard
from trullo.tconfig import TConfig
# logging.basicConfig(level=logging.DEBUG)
from trullo.trl_list import TrlList
from trullo.usecases import Usecases

logging.basicConfig(level=logging.INFO)
logging.getLogger('urllib3.connectionpool').setLevel(logging.INFO)

logger = logging.getLogger(__name__)


def edit_card(card_to_edit: TrlCard = None) -> (str, str):
    """
    :param card_to_edit:
    :return: a Tuple with the new name and description of the card
    """
    tempfile_suffix = 'newcard.md'
    clean_card_name = 'New Card Title'
    card_description = 'New Card Description'
    if card_to_edit is not None:
        tempfile_suffix = f'{card.id}.md'
        clean_card_name = str(card_to_edit.raw_data['name']).replace('\n', '')
        card_description = card_to_edit.raw_data['desc']

    tmpfile_path = f'{tempfile.gettempdir()}/.trl-{tempfile_suffix}'
    with open(tmpfile_path, 'w') as fd:
        fd.writelines(
            f"# The line below is the card title, "
            f"lines after that are the card description\n"
            f"{clean_card_name}\n{card_description}")
    subprocess.Popen([os.environ.get('EDITOR'), tmpfile_path]).wait()
    with open(tmpfile_path, 'r') as fd:
        lines = fd.readlines()
    return urllib.parse.quote(lines[1].replace('\n', ''), safe=''), \
           urllib.parse.quote(str.join('', lines[2:]), safe='')


if __name__ == '__main__':
    args = docopt(__doc__, version='Trullo beta')



@@ 102,10 70,13 @@ if __name__ == '__main__':

    tmpdir = tempfile.gettempdir()
    selected_board_filepath = f'{tmpdir}/.trl-selected-board'
    if os.path.exists(selected_board_filepath):
        with open(selected_board_filepath, 'r') as fh:
            selected_board_id, selected_board_name = fh.readline().split(
                ' ', 1)

    usecases = Usecases(TConfig(selected_board_filepath),
                        TClient(),
                        Shortener(),
                        Printer())

    selected_board_id, selected_board_name = usecases.get_selected_board()

    if args['g']:
        api_path = args['<api_path>']


@@ 113,115 84,40 @@ if __name__ == '__main__':
        pp.pprint(tclient.get(api_path))

    if args['b']:
        boards = tclient.get_boards()
        if args['<board_shortcut>']:
            board_shortcut = args['<board_shortcut>']
            matching_boards: List[TrlBoard] = Shortener.get_matches(
                board_shortcut,
                boards
            )
            if len(matching_boards) > 1:
                matching_names = \
                    [board.get_normalized_name() for board in matching_boards]
                print(
                    f'shortcut [{board_shortcut}] matches more than one board: '
                    f'{matching_names}')
                exit(1)
            board = matching_boards[0]
            print(f'selected board {board.raw_data["name"]}')
            with open(selected_board_filepath, 'w') as fh:
                fh.write(f'{board.id} {board.raw_data["name"]}')
            usecases.select_board(board_shortcut)
        else:
            Printer.print_boards(boards)
            if os.path.exists(selected_board_filepath):
                print(f'\ncurrently selected board: {selected_board_name}')
            else:
                print(f'\nselect a board with `trl b <board_shortcut>`')
            usecases.print_board_list()

    # stuff that works only if a board is selected
    if not args['b'] and not os.path.exists(selected_board_filepath):
    if not args['b'] and selected_board_name is None:
        print(f'first select a board with `trl b`')
        exit(1)

    if args['ll']:
        board = tclient.get_board(selected_board_id)
        Printer.print_board_lists(board)
        usecases.print_board_lists()

    if args['l']:
        board = tclient.get_board(selected_board_id)
        list_shortcuts = args['<list_shortcuts>']
        if list_shortcuts is not None and len(list_shortcuts) > 0:
            Printer.print_board(board, list_shortcuts)
        else:
            Printer.print_board(board)
        usecases.print_lists(list_shortcuts)

    if args['c']:
        board = tclient.get_board(selected_board_id)
        new_command = args['n']
        if new_command:
            target_list_shortcut = args['<list_shortcut>']
            matching_lists: List[TrlList] = Shortener.get_matches(
                target_list_shortcut,
                board.lists
            )
            if len(matching_lists) > 1:
                matching_names = \
                    [list_.get_normalized_name() for list_ in matching_lists]
                print(
                    f'shortcut [{target_list_shortcut}] '
                    f'matches more than one list: '
                    f'{matching_names}')
                exit(1)
            list_id = matching_lists[0].id

            new_card_name, new_card_desc = edit_card()
            tclient.new_card(list_id, new_card_name, new_card_desc)
            usecases.create_card(target_list_shortcut)
        else:
            card_shortcut = args['<card_shortcut>']
            matching_cards: List[TrlCard] = Shortener.get_matches(
                card_shortcut,
                board.cards
            )

            if len(matching_cards) > 1:
                matching_names = \
                    [matching_card.get_normalized_name()
                     for matching_card in matching_cards]
                print(
                    f'shortcut [{card_shortcut}] '
                    f'matches more than one card: '
                    f'{matching_names}')
                exit(1)

            selected_card = matching_cards[0]
            card = tclient.get_card(selected_card.id)

            open_command = args['o']
            move_command = args['m']
            edit_command = args['e']
            if open_command:
                subprocess.Popen(['xdg-open', card.raw_data['shortUrl']])
                usecases.open_card_in_browser(card_shortcut)
            elif move_command:
                target_list_shortcut = args['<list_shortcut>']
                matching_lists: List[TrlList] = Shortener.get_matches(
                    target_list_shortcut,
                    board.lists
                )
                if len(matching_lists) > 1:
                    matching_names = \
                        [list_.get_normalized_name() for list_ in
                         matching_lists]
                    print(
                        f'shortcut [{target_list_shortcut}] '
                        f'matches more than one list: '
                        f'{matching_names}')
                    exit(1)
                list_id = matching_lists[0].id

                tclient.move_card(card.id, list_id)
                usecases.move_card(card_shortcut, target_list_shortcut)
            elif edit_command:
                card_new_name, card_new_desc = edit_card(card)
                logger.debug(card_new_desc)
                tclient.edit_card(card.id, card_new_name, card_new_desc)
                usecases.update_card(card_shortcut)
            else:
                Printer.print_card(card)
                usecases.print_card(card_shortcut)

A trullo/tconfig.py => trullo/tconfig.py +6 -0
@@ 0,0 1,6 @@
import attr


@attr.s(auto_attribs=True)
class TConfig:
    selected_board_filepath: str

A trullo/usecases.py => trullo/usecases.py +190 -0
@@ 0,0 1,190 @@
import os
import subprocess
import tempfile
import urllib
from typing import Optional, Tuple, List

import attr

from trullo.printer import Printer
from trullo.shortener import Shortener
from trullo.tclient import TClient
from trullo.tconfig import TConfig
from trullo.trl_board import TrlBoard
from trullo.trl_card import TrlCard
from trullo.trl_list import TrlList


@attr.s(auto_attribs=True)
class Usecases:
    tconfig: TConfig
    tclient: TClient
    shortener: Shortener
    printer: Printer
    selected_board_id: Optional[str] = attr.ib(default=None)
    selected_board_name: Optional[str] = attr.ib(default=None)

    def get_selected_board(self) -> Optional[Tuple[str, str]]:
        if os.path.exists(self.tconfig.selected_board_filepath):
            with open(self.tconfig.selected_board_filepath, 'r') as fh:
                selected_board_id, selected_board_name = \
                    fh.readline().split(' ', 1)
                self.selected_board_id = selected_board_id
                self.selected_board_name = selected_board_name
                return selected_board_id, selected_board_name

    def print_board_list(self):
        boards = self.tclient.get_boards()
        self.printer.print_boards(boards)
        if os.path.exists(self.tconfig.selected_board_filepath):
            print(f'\ncurrently selected board: {self.selected_board_name}')
        else:
            print(f'\nselect a board with `trl b <board_shortcut>`')

    def select_board(self, board_shortcut: str):
        boards = self.tclient.get_boards()
        matching_boards: List[TrlBoard] = self.shortener.get_matches(
            board_shortcut,
            boards
        )
        if len(matching_boards) > 1:
            matching_names = \
                [board.get_normalized_name() for board in matching_boards]
            print(
                f'shortcut [{board_shortcut}] matches more than one board: '
                f'{matching_names}')
            exit(1)
        board = matching_boards[0]
        print(f'selected board {board.raw_data["name"]}')
        with open(self.tconfig.selected_board_filepath, 'w') as fh:
            fh.write(f'{board.id} {board.raw_data["name"]}')

    def print_board_lists(self):
        board = self.tclient.get_board(self.selected_board_id)
        self.printer.print_board_lists(board)

    def print_lists(self, lists_shortcuts: Optional[List[str]]):
        board = self.tclient.get_board(self.selected_board_id)
        if lists_shortcuts is not None and len(lists_shortcuts) > 0:
            Printer.print_board(board, lists_shortcuts)
        else:
            Printer.print_board(board)

    def print_card(self, card_shortcut: str):
        card = self._get_card(card_shortcut)
        self.printer.print_card(card)

    def open_card_in_browser(self, card_shortcut: str):
        card = self._get_card(card_shortcut)
        subprocess.Popen(['xdg-open', card.raw_data['shortUrl']])

    def create_card(self, target_list_shortcut: str):
        board = self.tclient.get_board(self.selected_board_id)
        matching_lists: List[TrlList] = Shortener.get_matches(
            target_list_shortcut,
            board.lists
        )
        if len(matching_lists) > 1:
            matching_names = \
                [list_.get_normalized_name() for list_ in matching_lists]
            print(
                f'shortcut [{target_list_shortcut}] '
                f'matches more than one list: '
                f'{matching_names}')
            exit(1)

        if len(matching_lists) == 0:
            print(
                f'shortcut [{target_list_shortcut}] '
                f'does not match any list')
            exit(1)

        list_id = matching_lists[0].id

        new_card_name, new_card_desc = self._edit_card()
        self.tclient.new_card(list_id, new_card_name, new_card_desc)

    def update_card(self, card_shortcut: str):
        card = self._get_card(card_shortcut)
        card_new_name, card_new_desc = self._edit_card(card)
        self.tclient.edit_card(card.id, card_new_name, card_new_desc)

    def move_card(self, card_shortcut: str, target_list_shortcut: str):
        board = self.tclient.get_board(self.selected_board_id)
        matching_lists: List[TrlList] = Shortener.get_matches(
            target_list_shortcut,
            board.lists
        )
        if len(matching_lists) > 1:
            matching_names = \
                [list_.get_normalized_name() for list_ in
                 matching_lists]
            print(
                f'shortcut [{target_list_shortcut}] '
                f'matches more than one list: '
                f'{matching_names}')
            exit(1)

        if len(matching_lists) == 0:
            print(
                f'shortcut [{target_list_shortcut}] '
                f'does not match any list')
            exit(1)

        list_id = matching_lists[0].id

        card = self._get_card(card_shortcut)

        self.tclient.move_card(card.id, list_id)

    def _get_card(self, card_shortcut):
        board = self.tclient.get_board(self.selected_board_id)
        matching_cards: List[TrlCard] = Shortener.get_matches(
            card_shortcut,
            board.cards
        )
        if len(matching_cards) > 1:
            matching_names = \
                [matching_card.get_normalized_name()
                 for matching_card in matching_cards]
            print(
                f'shortcut [{card_shortcut}] '
                f'matches more than one card: '
                f'{matching_names}')
            exit(1)

        if len(matching_cards) == 0:
            print(
                f'shortcut [{card_shortcut}] '
                f'does not match any cards')
            exit(1)

        selected_card = matching_cards[0]
        card = self.tclient.get_card(selected_card.id)
        return card

    def _edit_card(self, card_to_edit: TrlCard = None) -> (str, str):
        """
        :param card_to_edit:
        :return: a Tuple with the new name and description of the card
        """
        tempfile_suffix = 'newcard.md'
        clean_card_name = 'New Card Title'
        card_description = 'New Card Description'
        if card_to_edit is not None:
            tempfile_suffix = f'{card_to_edit.id}.md'
            clean_card_name = str(card_to_edit.raw_data['name']).replace('\n',
                                                                         '')
            card_description = card_to_edit.raw_data['desc']

        tmpfile_path = f'{tempfile.gettempdir()}/.trl-{tempfile_suffix}'
        with open(tmpfile_path, 'w') as fd:
            fd.writelines(
                f"# The line below is the card title, "
                f"lines after that are the card description\n"
                f"{clean_card_name}\n{card_description}")
        subprocess.Popen([os.environ.get('EDITOR'), tmpfile_path]).wait()
        with open(tmpfile_path, 'r') as fd:
            lines = fd.readlines()
        return urllib.parse.quote(lines[1].replace('\n', ''), safe=''), \
               urllib.parse.quote(str.join('', lines[2:]), safe='')