rtutil: Add RT.iter_urls() method.

This commit is contained in:
Brett Smith 2020-04-29 11:23:48 -04:00
parent 9fef177d2d
commit 5a1f7122bd
2 changed files with 132 additions and 43 deletions

View file

@ -271,55 +271,40 @@ class RT:
)
return self._extend_url(path_tail)
def _urls(self, links: Iterable[str]) -> Iterator[str]:
def exists(self, ticket_id: RTId, attachment_id: Optional[RTId]=None) -> bool:
return self.url(ticket_id, attachment_id) is not 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
This method iterates over metadata link strings (e.g., from
Metadata.get_links()) and transforms them for web presentation.
If the string is a valid RT reference, the corresponding web URL
will be formatted with ``rt_fmt``.
If the string is a well-formed RT reference but the object doesn't
exist, it will be formatted with ``missing_fmt``.
All other link strings will be formatted with ``nonrt_fmt``.
"""
for link in links:
parsed = self.parse(link)
if parsed is None:
yield link
yield nonrt_fmt.format(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
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)

View file

@ -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]',
])