Skip to content

Commit 28643ea

Browse files
authored
Generate publications list from BibTeX file (#1507)
* Generate publications list from BibTeX file * Add help strings to arguments and tidy up interface * Warn when entry does not have note with valid section name * Add pybtex to docs requirements * Add docstrings * Ignore generated publications list doc file * Use keywords rather than note field for section * Add title as additional sort key to break ties * Allow updating BibTeX file from Zotero at command line * Add requests to documentation requirements * Add link to download BibTeX file * Expandable publication list entries with additional details * Format publication details as table * Add tox environment for updating publications * Revert accidental deletion of sphinx-build command in tox * Strip trailing whitespace * Show defaults in script help and normalize arg name * Adding Healthcare provision section to publication list * Updating publication data from Zotero * Add published version of overview paper and remove pre-print from list
1 parent 630c639 commit 28643ea

6 files changed

Lines changed: 618 additions & 53 deletions

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,7 @@ src/**/_version.py
124124

125125
# Generated TLO docs files
126126
docs/_*.rst
127+
docs/_*.html
127128
docs/hsi_events.csv
128129
docs/parameters.rst
129130
docs/reference/modules.rst

docs/publications.bib

Lines changed: 362 additions & 0 deletions
Large diffs are not rendered by default.

docs/publications.rst

Lines changed: 3 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,60 +1,10 @@
1-
21
=============
32
Publications
43
=============
54

65
These are the publications that have been generated either in the course of the model's development or its application.
76

7+
:download:`Download a BibTeX file for all publications <./publications.bib>`
88

9-
Overview of the Model
10-
======================
11-
12-
* `A Healthcare Service Delivery and Epidemiological Model for Investigating Resource Allocation for Health: The Thanzi La Onse Model <https://www.medrxiv.org/content/10.1101/2024.01.04.24300834v1>`_
13-
14-
15-
Analyses Using The Model
16-
========================
17-
18-
* `The potential impact of declining development assistance for healthcare on population health: projections for Malawi <https://www.medrxiv.org/content/10.1101/2024.10.11.24315287v1>`_
19-
20-
* `Health workforce needs in Malawi: analysis of the Thanzi La Onse integrated epidemiological model of care <https://human-resources-health.biomedcentral.com/articles/10.1186/s12960-024-00949-2>`_
21-
22-
* `A new approach to Health Benefits Package design: an application of the Thanzi La Onse model in Malawi <https://journals.plos.org/ploscompbiol/article?id=10.1371/journal.pcbi.1012462>`_
23-
24-
* `The Changes in Health Service Utilisation in Malawi During the COVID-19 Pandemic <https://journals.plos.org/plosone/article?id=10.1371/journal.pone.0290823>`_
25-
26-
* `Modeling Contraception and Pregnancy in Malawi: A Thanzi La Onse Mathematical Modeling Study <https://onlinelibrary.wiley.com/doi/10.1111/sifp.12255>`_
27-
28-
* `Factors Associated with Consumable Stock-Outs in Malawi: Evidence from a Facility Census <https://www.sciencedirect.com/science/article/pii/S2214109X24000950>`_
29-
30-
* `The Effects of Health System Frailties on the Projected Impact of the HIV and TB Programmes in Malawi <https://www.sciencedirect.com/science/article/pii/S2214109X24002596>`_
31-
32-
* `Estimating the health burden of road traffic injuries in Malawi using an individual-based model <https://injepijournal.biomedcentral.com/articles/10.1186/s40621-022-00386-6>`_
33-
34-
* `The potential impact of intervention strategies on COVID-19 transmission in Malawi: A mathematical modelling study. <https://bmjopen.bmj.com/content/11/7/e045196>`_
35-
36-
* `The potential impact of including pre-school aged children in the praziquantel mass-drug administration programmes on the S.haematobium infections in Malawi: a modelling study <https://www.medrxiv.org/content/10.1101/2020.12.09.20246652v1>`_
37-
38-
* `A Decade of Progress in HIV, Malaria, and Tuberculosis Initiatives in Malawi. <https://www.medrxiv.org/content/10.1101/2024.10.08.24315077v1>`_
39-
40-
41-
Healthcare Seeking Behaviour
42-
============================
43-
44-
* `Socio-demographic factors associated with early antenatal care visits among pregnant women in Malawi: 2004–2016 <https://journals.plos.org/plosone/article?id=10.1371/journal.pone.0263650>`_
45-
46-
* `Factors associated with healthcare seeking behaviour for children in Malawi: 2016. <https://onlinelibrary.wiley.com/doi/abs/10.1111/tmi.13499>`_
47-
48-
* `A cross-sectional study on factors associated with health seeking behaviour of Malawians aged 15+ years in 2016. <https://www.ajol.info/index.php/mmj/article/view/202965>`_
49-
50-
51-
52-
53-
54-
55-
56-
57-
58-
59-
60-
9+
.. raw:: html
10+
:file: _publications_list.html

docs/requirements.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
sphinx>=1.3
22
sphinx-rtd-theme
3+
pybtex
34
pyyaml
5+
requests
46
tabulate

docs/tlo_publications.py

Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
"""Create publications page from BibTeX database file."""
2+
3+
import argparse
4+
import calendar
5+
from collections import defaultdict
6+
from pathlib import Path
7+
from warnings import warn
8+
9+
import pybtex.database
10+
import requests
11+
from pybtex.backends.html import Backend as HTMLBackend
12+
from pybtex.style.formatting import toplevel
13+
from pybtex.style.formatting.unsrt import Style as UnsrtStyle
14+
from pybtex.style.formatting.unsrt import date as publication_date
15+
from pybtex.style.names import BaseNameStyle, name_part
16+
from pybtex.style.sorting import BaseSortingStyle
17+
from pybtex.style.template import (
18+
FieldIsMissing,
19+
field,
20+
first_of,
21+
href,
22+
join,
23+
node,
24+
optional,
25+
sentence,
26+
tag,
27+
words,
28+
)
29+
30+
31+
class InlineHTMLBackend(HTMLBackend):
32+
"""Backend for bibliography output as plain list suitable for inclusion in a HTML document."""
33+
34+
def write_prologue(self):
35+
self.output("<ul style='list-style-type: none; padding-left: 0;'>\n")
36+
37+
def write_epilogue(self):
38+
self.output("</ul>\n")
39+
40+
def write_entry(self, _key, _label, text):
41+
self.output(f"<li style='list-style: none;'>{text}</li>\n")
42+
43+
44+
class DateSortingStyle(BaseSortingStyle):
45+
"""Sorting style for bibliography in reverse (newest first) publication date order."""
46+
47+
def sorting_key(self, entry):
48+
months = list(calendar.month_name)
49+
return (
50+
-int(entry.fields.get("year")),
51+
-months.index(entry.fields.get("month", "")),
52+
entry.fields.get("title", ""),
53+
)
54+
55+
56+
class LastOnlyNameStyle(BaseNameStyle):
57+
"""Name style showing only last names and associated name particles."""
58+
59+
def format(self, person, _abbr=False):
60+
return join[
61+
name_part(tie=True)[person.rich_prelast_names],
62+
name_part[person.rich_last_names],
63+
name_part(before=", ")[person.rich_lineage_names],
64+
]
65+
66+
67+
@node
68+
def summarized_names(children, context, role, summarize_limit=3, **kwargs):
69+
"""Return formatted names with et al. summarization when number exceeds specified limit."""
70+
71+
assert not children
72+
73+
try:
74+
persons = context["entry"].persons[role]
75+
except KeyError:
76+
raise FieldIsMissing(role, context["entry"])
77+
78+
name_style = LastOnlyNameStyle()
79+
if len(persons) > summarize_limit:
80+
return words[name_style.format(persons[0]), "et al."].format_data(context)
81+
else:
82+
formatted_names = [name_style.format(person) for person in persons]
83+
return join(**kwargs)[formatted_names].format_data(context)
84+
85+
86+
class SummarizedStyle(UnsrtStyle):
87+
"""
88+
Bibliography style showing summarized names, year, title and journal with expandable details.
89+
90+
Not suitable for use with LaTeX backend due to use of details tags.
91+
"""
92+
93+
default_sorting_style = DateSortingStyle
94+
95+
def _format_summarized_names(self, role):
96+
return summarized_names(role, sep=", ", sep2=" and ", last_sep=", and ")
97+
98+
def _format_label(self, label):
99+
return tag("em")[f"{label}: "]
100+
101+
def _format_details_as_table(self, details):
102+
return tag("table")[
103+
toplevel[
104+
*(
105+
tag("tr")[toplevel[tag("td")[tag("em")[key]], tag("td")[value]]]
106+
for key, value in details.items()
107+
)
108+
]
109+
]
110+
111+
def _get_summary_template(self, e, type_):
112+
venue_field = "journal" if type_ == "article" else "publisher"
113+
url = first_of[
114+
optional[join["https://doi.org/", field("doi", raw=True)]],
115+
optional[field("url", raw=True)],
116+
"#",
117+
]
118+
return href[
119+
url,
120+
sentence(sep=". ")[
121+
words[
122+
self._format_summarized_names("author"),
123+
optional["(", field("year"), ")"],
124+
],
125+
self.format_title(e, "title", as_sentence=False),
126+
tag("em")[field(venue_field)],
127+
],
128+
]
129+
130+
def _get_details_template(self, type_):
131+
bibtex_type_to_label = {"article": "Journal article", "misc": "Pre-print"}
132+
return self._format_details_as_table(
133+
{
134+
"Type": bibtex_type_to_label[type_],
135+
"DOI": optional[field("doi")],
136+
"Date": publication_date,
137+
"Authors": self.format_names("author"),
138+
"Abstract": field("abstract"),
139+
}
140+
)
141+
142+
def _get_summarized_template(self, e, type_):
143+
summary_template = self._get_summary_template(e, type_)
144+
details_template = self._get_details_template(type_)
145+
return tag("details")[tag("summary")[summary_template], details_template]
146+
147+
def get_article_template(self, e):
148+
return self._get_summarized_template(e, "article")
149+
150+
def get_misc_template(self, e):
151+
return self._get_summarized_template(e, "misc")
152+
153+
154+
def write_publications_list(stream, bibliography_data, section_names, backend, style):
155+
"""Write bibliography data with given backend and style to a stream splitting in to sections."""
156+
keys_by_section = defaultdict(list)
157+
for key, entry in bibliography_data.entries.items():
158+
keywords = set(k.strip() for k in entry.fields.get("keywords", "").split(","))
159+
section_names_in_keywords = keywords & set(section_names)
160+
if len(section_names_in_keywords) == 1:
161+
keys_by_section[section_names_in_keywords.pop()].append(key)
162+
elif len(section_names_in_keywords) == 0:
163+
msg = (
164+
f"BibTeX entry with key {key} does not have a keyword / tag corresponding to "
165+
f"one of section names {section_names} and so will not be included in output."
166+
)
167+
warn(msg, stacklevel=2)
168+
else:
169+
msg = (
170+
f"BibTeX entry with key {key} has multiple keywords / tags corresponding to "
171+
f"section names {section_names} and so will not be included in output."
172+
)
173+
warn(msg, stacklevel=2)
174+
for section_name in section_names:
175+
stream.write(f"<h2>{section_name}</h2>\n")
176+
formatted_bibliography = style.format_bibliography(
177+
bibliography_data, keys_by_section[section_name]
178+
)
179+
backend.write_to_stream(formatted_bibliography, stream)
180+
stream.write("\n")
181+
182+
183+
if __name__ == "__main__":
184+
docs_directory = Path(__file__).parent
185+
parser = argparse.ArgumentParser(
186+
description=__doc__, formatter_class=argparse.ArgumentDefaultsHelpFormatter
187+
)
188+
parser.add_argument(
189+
"--bib-file",
190+
type=Path,
191+
default=docs_directory / "publications.bib",
192+
help="BibTeX file containing publication details",
193+
)
194+
parser.add_argument(
195+
"--output-file",
196+
type=Path,
197+
default=docs_directory / "_publications_list.html",
198+
help="File to write publication list to in HTML format",
199+
)
200+
parser.add_argument(
201+
"--update-from-zotero",
202+
action="store_true",
203+
help="Update BibTeX file at path specified by --bib-file from Zotero group library",
204+
)
205+
parser.add_argument(
206+
"--zotero-group-id",
207+
default="5746396",
208+
help="Integer identifier for Zotero group library",
209+
)
210+
args = parser.parse_args()
211+
if args.update_from_zotero:
212+
endpoint_url = f"https://api.zotero.org/groups/{args.zotero_group_id}/items"
213+
# Zotero API requires maximum number of results to return (limit parameter)
214+
# to be explicitly specified for export formats such as bibtex and allows a
215+
# maximum value of 100 - if we exceed this number of publications will need
216+
# to switch to making multiple requests with different start indices
217+
response = requests.get(
218+
endpoint_url, params={"format": "bibtex", "limit": "100"}
219+
)
220+
if response.ok:
221+
with open(args.bib_file, "w") as bib_file:
222+
bib_file.write(response.text)
223+
else:
224+
msg = (
225+
f"Request to {endpoint_url} failed with status code "
226+
f"{response.status_code} ({response.reason})"
227+
)
228+
raise RuntimeError(msg)
229+
with open(args.output_file, "w") as output_file:
230+
write_publications_list(
231+
stream=output_file,
232+
bibliography_data=pybtex.database.parse_file(args.bib_file),
233+
section_names=[
234+
"Overview of the model",
235+
"Analyses using the model",
236+
"Healthcare seeking behaviour",
237+
"Healthcare provision",
238+
],
239+
backend=InlineHTMLBackend(),
240+
style=SummarizedStyle(),
241+
)

tox.ini

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,8 @@ commands =
7373
python docs/tlo_data_sources.py
7474
; Generate contributors page
7575
python docs/tlo_contributors.py
76+
; Generate publications page
77+
python docs/tlo_publications.py
7678
; Generate resources files page
7779
python docs/tlo_resources.py
7880
; Generate HSI events listing
@@ -139,6 +141,13 @@ commands = python {toxinidir}/src/scripts/automation/update_citation.py
139141
skip_install = true
140142
deps = pyyaml
141143

144+
[testenv:update-publications]
145+
commands = python {toxinidir}/docs/tlo_publications.py --update-from-zotero
146+
skip_install = true
147+
deps =
148+
pybtex
149+
requests
150+
142151
[testenv:requirements]
143152
commands =
144153
pip-compile --output-file {toxinidir}/requirements/base.txt

0 commit comments

Comments
 (0)