From 999ca2c5e1fafee91ef7b518da208d02c229d595 Mon Sep 17 00:00:00 2001 From: Brett Smith Date: Tue, 28 Apr 2020 16:20:25 -0400 Subject: [PATCH] rtutil: Add RT.txn_with_urls() method. --- conservancy_beancount/rtutil.py | 55 +++++++++++++++++++++++++++++++++ tests/test_rtutil.py | 37 ++++++++++++++++++++++ 2 files changed, 92 insertions(+) diff --git a/conservancy_beancount/rtutil.py b/conservancy_beancount/rtutil.py index f8c7974..183cd1e 100644 --- a/conservancy_beancount/rtutil.py +++ b/conservancy_beancount/rtutil.py @@ -25,8 +25,13 @@ import rt from pathlib import Path +from . import data +from beancount.core import data as bc_data + from typing import ( + overload, Callable, + Iterable, Iterator, MutableMapping, Optional, @@ -34,6 +39,9 @@ from typing import ( Tuple, Union, ) +from .beancount_types import ( + Transaction, +) RTId = Union[int, str] TicketAttachmentIds = Tuple[str, Optional[str]] @@ -263,6 +271,53 @@ class RT: ) return self._extend_url(path_tail) + def _urls(self, links: Iterable[str]) -> Iterator[str]: + for link in links: + parsed = self.parse(link) + if parsed is None: + yield link + else: + ticket_id, attachment_id = parsed + url = self.url(ticket_id, attachment_id) + yield f'<{url}>' + + @overload + def _meta_with_urls(self, meta: None) -> None: ... + + @overload + def _meta_with_urls(self, meta: bc_data.Meta) -> bc_data.Meta: ... + + def _meta_with_urls(self, meta: Optional[bc_data.Meta]) -> Optional[bc_data.Meta]: + if meta is None: + return None + link_meta = data.Metadata(meta) + retval = meta.copy() + for key in data.LINK_METADATA: + try: + links = link_meta.get_links(key) + except TypeError: + continue + if links: + retval[key] = ' '.join(self._urls(links)) + return retval + + def txn_with_urls(self, txn: Transaction) -> Transaction: + """Copy a transaction with RT references replaced with web URLs + + Given a Beancount Transaction, this method returns a Transaction + that's identical, except any references to RT in the metadata for + the Transaction and its Postings are replaced with web URLs. + This is useful for reporting tools that want to format the + transaction with URLs that are recognizable by other tools. + """ + # mypy doesn't recognize that postings is a valid argument, probably a + # bug in the NamedTuple→Directive→Transaction hierarchy. + return txn._replace( # type:ignore[call-arg] + meta=self._meta_with_urls(txn.meta), + postings=[post._replace(meta=self._meta_with_urls(post.meta)) + for post in txn.postings], + ) + def exists(self, ticket_id: RTId, attachment_id: Optional[RTId]=None) -> bool: return self.url(ticket_id, attachment_id) is not None diff --git a/tests/test_rtutil.py b/tests/test_rtutil.py index 1fed60f..70b09c7 100644 --- a/tests/test_rtutil.py +++ b/tests/test_rtutil.py @@ -39,6 +39,11 @@ EXPECTED_URLS = [ (9, None, None), ] +EXPECTED_URLS_MAP = { + (ticket_id, attachment_id): url + for ticket_id, attachment_id, url in EXPECTED_URLS +} + @pytest.fixture(scope='module') def rt(): client = testutil.RTClient() @@ -203,3 +208,35 @@ def test_results_not_found_only_in_transient_cache(new_client): new_client.TICKET_DATA['9'] = [('99', '(Unnamed)', 'text/plain', '0b')] assert not rt1.exists(9) assert rt2.exists(9) + +def test_txn_with_urls(new_client): + txn_meta = { + 'rt-id': 'rt:1', + 'contract': 'RepoLink.pdf', + 'statement': 'doc1.txt rt:1/4 doc2.txt', + } + txn = testutil.Transaction(**txn_meta, postings=[ + ('Income:Donations', -10, {'receipt': 'rt:2/13 donation.txt'}), + ('Assets:Cash', 10, {'receipt': 'cash.png rt:2/14'}), + ]) + rt = rtutil.RT(new_client) + actual = rt.txn_with_urls(txn) + def check(source, key, ticket_id, attachment_id=None): + url_path = EXPECTED_URLS_MAP[(ticket_id, attachment_id)] + assert f'<{DEFAULT_RT_URL}{url_path}>' in source.meta[key] + expected_keys = set(txn_meta) + expected_keys.update(['filename', 'lineno']) + assert set(actual.meta) == expected_keys + check(actual, 'rt-id', 1) + assert actual.meta['contract'] == txn_meta['contract'] + assert actual.meta['statement'].startswith('doc1.txt ') + check(actual, 'statement', 1, 4) + check(actual.postings[0], 'receipt', 2, 13) + assert actual.postings[0].meta['receipt'].endswith(' donation.txt') + check(actual.postings[1], 'receipt', 2, 14) + assert actual.postings[1].meta['receipt'].startswith('cash.png ') + # Check the original transaction is unchanged + for key, expected in txn_meta.items(): + assert txn.meta[key] == expected + assert txn.postings[0].meta['receipt'] == 'rt:2/13 donation.txt' + assert txn.postings[1].meta['receipt'] == 'cash.png rt:2/14'