rtutil: Add RT.iter_urls() method.
This commit is contained in:
parent
9fef177d2d
commit
5a1f7122bd
2 changed files with 132 additions and 43 deletions
|
@ -271,55 +271,40 @@ class RT:
|
||||||
)
|
)
|
||||||
return self._extend_url(path_tail)
|
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:
|
for link in links:
|
||||||
parsed = self.parse(link)
|
parsed = self.parse(link)
|
||||||
if parsed is None:
|
if parsed is None:
|
||||||
yield link
|
yield nonrt_fmt.format(link)
|
||||||
else:
|
else:
|
||||||
ticket_id, attachment_id = parsed
|
ticket_id, attachment_id = parsed
|
||||||
url = self.url(ticket_id, attachment_id)
|
url = self.url(ticket_id, attachment_id)
|
||||||
yield f'<{url}>'
|
if url is None:
|
||||||
|
yield missing_fmt.format(link)
|
||||||
@overload
|
else:
|
||||||
def _meta_with_urls(self, meta: None) -> None: ...
|
yield rt_fmt.format(url)
|
||||||
|
|
||||||
@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
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def metadata_regexp(self,
|
def metadata_regexp(self,
|
||||||
|
@ -365,6 +350,68 @@ class RT:
|
||||||
return None
|
return None
|
||||||
return self._ticket_url(ticket_id)
|
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]:
|
def url(self, ticket_id: RTId, attachment_id: Optional[RTId]=None) -> Optional[str]:
|
||||||
if attachment_id is None:
|
if attachment_id is None:
|
||||||
return self.ticket_url(ticket_id)
|
return self.ticket_url(ticket_id)
|
||||||
|
|
|
@ -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)
|
expected = '{}Ticket/Attachment/9/9/RT1%20attachment%209.{}'.format(DEFAULT_RT_URL, extension)
|
||||||
assert rt.url(1, 9) == expected
|
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)
|
@pytest.mark.parametrize('ticket_id,attachment_id,expected', EXPECTED_URLS)
|
||||||
def test_exists(rt, ticket_id, attachment_id, expected):
|
def test_exists(rt, ticket_id, attachment_id, expected):
|
||||||
expected = False if expected is None else True
|
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.meta[key] == expected
|
||||||
assert txn.postings[0].meta['receipt'] == 'rt:2/13 donation.txt'
|
assert txn.postings[0].meta['receipt'] == 'rt:2/13 donation.txt'
|
||||||
assert txn.postings[1].meta['receipt'] == 'cash.png rt:2/14'
|
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]',
|
||||||
|
])
|
||||||
|
|
Loading…
Reference in a new issue