Changeset - 5a1f7122bd3d
[Not reviewed]
0 2 0
Brett Smith - 4 years ago 2020-04-29 15:23:48
brettcsmith@brettcsmith.org
rtutil: Add RT.iter_urls() method.
2 files changed with 132 insertions and 43 deletions:
0 comments (0 inline, 0 general)
conservancy_beancount/rtutil.py
Show inline comments
...
 
@@ -271,55 +271,40 @@ 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}>'
 
    def exists(self, ticket_id: RTId, attachment_id: Optional[RTId]=None) -> bool:
 
        return self.url(ticket_id, attachment_id) is not None
 

	
 
    @overload
 
    def _meta_with_urls(self, meta: None) -> None: ...
 
    def iter_urls(self,
 
                  links: Iterable[str],
 
                  rt_fmt: str='{}',
 
                  nonrt_fmt: str='{}',
 
                  missing_fmt: str='{}',
 
    ) -> Iterator[str]:
 
        """Iterate over metadata links, replacing RT references with web URLs
 

	
 
    @overload
 
    def _meta_with_urls(self, meta: bc_data.Meta) -> bc_data.Meta: ...
 
        This method iterates over metadata link strings (e.g., from
 
        Metadata.get_links()) and transforms them for web presentation.
 

	
 
    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
 
        If the string is a valid RT reference, the corresponding web URL
 
        will be formatted with ``rt_fmt``.
 

	
 
    def txn_with_urls(self, txn: Transaction) -> Transaction:
 
        """Copy a transaction with RT references replaced with web URLs
 
        If the string is a well-formed RT reference but the object doesn't
 
        exist, it will be formatted with ``missing_fmt``.
 

	
 
        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],
 
        )
 
        All other link strings will be formatted with ``nonrt_fmt``.
 

	
 
    def exists(self, ticket_id: RTId, attachment_id: Optional[RTId]=None) -> bool:
 
        return self.url(ticket_id, attachment_id) is not None
 
        """
 
        for link in links:
 
            parsed = self.parse(link)
 
            if parsed is None:
 
                yield nonrt_fmt.format(link)
 
            else:
 
                ticket_id, attachment_id = parsed
 
                url = self.url(ticket_id, attachment_id)
 
                if url is None:
 
                    yield missing_fmt.format(link)
 
                else:
 
                    yield rt_fmt.format(url)
 

	
 
    @classmethod
 
    def metadata_regexp(self,
...
 
@@ -365,6 +350,68 @@ class RT:
 
            return None
 
        return self._ticket_url(ticket_id)
 

	
 
    @overload
 
    def _meta_with_urls(self,
 
                        meta: None,
 
                        rt_fmt: str,
 
                        nonrt_fmt: str,
 
                        missing_fmt: str,
 
    ) -> None: ...
 

	
 
    @overload
 
    def _meta_with_urls(self,
 
                        meta: bc_data.Meta,
 
                        rt_fmt: str,
 
                        nonrt_fmt: str,
 
                        missing_fmt: str,
 
    ) -> bc_data.Meta: ...
 

	
 
    def _meta_with_urls(self,
 
                        meta: Optional[bc_data.Meta],
 
                        rt_fmt: str,
 
                        nonrt_fmt: str,
 
                        missing_fmt: str,
 
    ) -> 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:
 
                links = ()
 
            if links:
 
                retval[key] = ' '.join(self.iter_urls(
 
                    links, rt_fmt, nonrt_fmt, missing_fmt,
 
                ))
 
        return retval
 

	
 
    def txn_with_urls(self, txn: Transaction,
 
                      rt_fmt: str='<{}>',
 
                      nonrt_fmt: str='{}',
 
                      missing_fmt: str='{}',
 
    ) -> 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.
 

	
 
        The format string arguments have the same meaning as RT.iter_urls().
 
        See that docstring for details.
 
        """
 
        # 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, rt_fmt, nonrt_fmt, missing_fmt),
 
            postings=[post._replace(meta=self._meta_with_urls(
 
                post.meta, rt_fmt, nonrt_fmt, missing_fmt,
 
            )) for post in txn.postings],
 
        )
 

	
 
    def url(self, ticket_id: RTId, attachment_id: Optional[RTId]=None) -> Optional[str]:
 
        if attachment_id is None:
 
            return self.ticket_url(ticket_id)
tests/test_rtutil.py
Show inline comments
...
 
@@ -118,6 +118,30 @@ def test_url_default_filename(new_client, mimetype, extension):
 
    expected = '{}Ticket/Attachment/9/9/RT1%20attachment%209.{}'.format(DEFAULT_RT_URL, extension)
 
    assert rt.url(1, 9) == expected
 

	
 
@pytest.mark.parametrize('rt_fmt,nonrt_fmt,missing_fmt', [
 
    ('{}', '{}', '{}',),
 
    ('<{}>', '[{}]', '({})'),
 
])
 
def test_iter_urls(rt, rt_fmt, nonrt_fmt, missing_fmt):
 
    expected_map = {
 
        'rt:{}{}'.format(tid, '' if aid is None else f'/{aid}'): url
 
        for tid, aid, url in EXPECTED_URLS
 
    }
 
    expected_map['https://example.com'] = None
 
    expected_map['invoice.pdf'] = None
 
    keys = list(expected_map)
 
    urls = rt.iter_urls(keys, rt_fmt, nonrt_fmt, missing_fmt)
 
    for key, actual in itertools.zip_longest(keys, urls):
 
        expected = expected_map[key]
 
        if expected is None:
 
            if key.startswith('rt:'):
 
                expected = missing_fmt.format(key)
 
            else:
 
                expected = nonrt_fmt.format(key)
 
        else:
 
            expected = rt_fmt.format(DEFAULT_RT_URL + expected)
 
        assert actual == expected
 

	
 
@pytest.mark.parametrize('ticket_id,attachment_id,expected', EXPECTED_URLS)
 
def test_exists(rt, ticket_id, attachment_id, expected):
 
    expected = False if expected is None else True
...
 
@@ -239,3 +263,21 @@ def test_txn_with_urls(rt):
 
        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'
 

	
 
def test_txn_with_urls_with_fmts(rt):
 
    txn_meta = {
 
        'rt-id': 'rt:1',
 
        'contract': 'RepoLink.pdf',
 
        'statement': 'rt:1/99 rt:1/4 stmt.txt',
 
    }
 
    txn = testutil.Transaction(**txn_meta)
 
    actual = rt.txn_with_urls(txn, '<{}>', '[{}]', '({})')
 
    rt_id_path = EXPECTED_URLS_MAP[(1, None)]
 
    assert actual.meta['rt-id'] == f'<{DEFAULT_RT_URL}{rt_id_path}>'
 
    assert actual.meta['contract'] == '[RepoLink.pdf]'
 
    statement_path = EXPECTED_URLS_MAP[(1, 4)]
 
    assert actual.meta['statement'] == ' '.join([
 
        '(rt:1/99)',
 
        f'<{DEFAULT_RT_URL}{statement_path}>',
 
        '[stmt.txt]',
 
    ])
0 comments (0 inline, 0 general)