1
0
mirror of https://github.com/msberends/AMR.git synced 2026-07-16 22:30:56 +02:00

3 Commits

8 changed files with 255 additions and 58 deletions

View File

@@ -29,7 +29,6 @@
on:
push:
# only on main
branches: "main"
name: Update TODO Tracker
@@ -40,40 +39,228 @@ jobs:
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # full history required for git blame
- name: Generate TODO list from R/
- name: Generate TODO report
env:
GH_TOKEN: ${{ secrets.GH_REPO_SCOPE }}
run: |
set -euo pipefail
export TZ=Europe/Amsterdam
last_updated=$(date +"%e %B %Y %H:%M:%S %Z" | sed 's/^ *//')
echo "## \`TODO\` Report" > todo.md
echo "" >> todo.md
echo "**Last Updated: ${last_updated}**" >> todo.md
echo "" >> todo.md
echo "_This overview is automatically updated on each push to \`main\`. It provides an automated overview of all mentions of the text \`TODO\`._" >> todo.md
echo "" >> todo.md
todos=$(grep -rn --include=\*.{R,Rmd,yaml,yml,md,css,js} --exclude={todo-tracker.yml,todo.md} "TODO" . || true)
if [ -z "$todos" ]; then
echo "✅ No TODOs found." >> todo.md
else
echo "$todos" | awk -F: -v repo="https://github.com/msberends/AMR/blob/main/" '
{
file = $1
gsub("^\\./", "", file) # remove leading ./ if present
line = $2
text = substr($0, index($0,$3))
if (file != last_file) {
if (last_file != "") print "```"
print ""
print "### [`" file "`](" repo file ")"
print "```r"
last_file = file
}
printf "L%s: %s\n", line, text
}
' >> todo.md
echo "\`\`\`" >> todo.md
REPO="msberends/AMR"
REPO_URL="https://github.com/$REPO/blob/main"
NOW=$(date +%s)
LAST_UPDATED=$(date +"%e %B %Y %H:%M:%S %Z" | sed 's/^ *//')
STALE_DAYS=180
# ── helper: human-readable age ──────────────────────────────
format_age() {
local d=$1
if [ "$d" -lt 0 ] 2>/dev/null; then echo "unknown"; return; fi
local y=$((d / 365)) m=$(( (d % 365) / 30 ))
if [ "$y" -gt 0 ] && [ "$m" -gt 0 ]; then echo "${y}y ${m}m"
elif [ "$y" -gt 0 ]; then echo "${y}y"
elif [ "$m" -gt 0 ]; then echo "${m}m"
else echo "${d}d"
fi
}
export -f format_age
# ── step 1: find all markers ────────────────────────────────
grep -rn \
--include='*.R' --include='*.Rmd' --include='*.yaml' \
--include='*.yml' --include='*.md' --include='*.css' \
--include='*.js' \
--exclude='todo-tracker.yml' --exclude='todo.md' \
-E '\b(TODO|FIXME|HACK|XXX)\b' . > /tmp/raw.txt || true
if [ ! -s /tmp/raw.txt ]; then
echo -e "## \`TODO\` Report\n\n**Last Updated: ${LAST_UPDATED}**\n\nNo markers found." > todo.md
exit 0
fi
# ── step 2: enrich with git blame & extract issue refs ──────
> /tmp/enriched.tsv
> /tmp/issues_seen.txt
while IFS= read -r match; do
clean=$(printf '%s\n' "$match" | sed 's|^\./||')
file=$(printf '%s\n' "$clean" | cut -d: -f1)
lineno=$(printf '%s\n' "$clean" | cut -d: -f2)
text=$(printf '%s\n' "$clean" | cut -d: -f3-)
# determine marker type (first match wins, TODO is default)
marker="TODO"
for m in FIXME HACK XXX; do
if printf '%s\n' "$text" | grep -qw "$m"; then marker="$m"; break; fi
done
# git blame timestamp
blame_ts=$(git blame -L "${lineno},${lineno}" --porcelain -- "$file" 2>/dev/null \
| awk '/^author-time/{print $2}' || echo "0")
blame_ts=${blame_ts:-0}
if [ "$blame_ts" -gt 0 ] 2>/dev/null; then
age_days=$(( (NOW - blame_ts) / 86400 ))
else
age_days=-1
fi
# extract issue references (#NNN)
issues=$(printf '%s\n' "$text" | grep -oE '#[0-9]+' | sed 's/#//' | tr '\n' ',' | sed 's/,$//' || true)
if [ -n "$issues" ]; then
for inum in $(echo "$issues" | tr ',' ' '); do
echo "$inum" >> /tmp/issues_seen.txt
done
fi
printf '%s\t%s\t%s\t%s\t%s\t%s\n' \
"$file" "$lineno" "$marker" "$age_days" "$issues" "$text" >> /tmp/enriched.tsv
done < /tmp/raw.txt
# ── step 3: query GitHub API for referenced issues ──────────
> /tmp/issue_info.tsv
if [ -s /tmp/issues_seen.txt ]; then
sort -un /tmp/issues_seen.txt | while read -r inum; do
info=$(gh api "/repos/$REPO/issues/$inum" \
--jq '"\(.state)\t\(.title)"' 2>/dev/null \
|| echo "unknown (could not fetch)")
printf '%s\t%s\n' "$inum" "$info" >> /tmp/issue_info.tsv
done
fi
# ── step 4: build the report ────────────────────────────────
{
# ── header ──
echo "## \`TODO\` Report"
echo ""
echo "**Last Updated: ${LAST_UPDATED}**"
echo ""
echo "_This overview is automatically updated on each push to \`main\`. It scans for \`TODO\`, \`FIXME\`, \`HACK\`, and \`XXX\` markers across the codebase._"
echo ""
# ── summary table ──
total=$(wc -l < /tmp/enriched.tsv | tr -d ' ')
files_affected=$(awk -F'\t' '{print $1}' /tmp/enriched.tsv | sort -u | wc -l | tr -d ' ')
todo_n=$(awk -F'\t' '$3=="TODO"' /tmp/enriched.tsv | wc -l | tr -d ' ')
fixme_n=$(awk -F'\t' '$3=="FIXME"' /tmp/enriched.tsv | wc -l | tr -d ' ')
hack_n=$(awk -F'\t' '$3=="HACK"' /tmp/enriched.tsv | wc -l | tr -d ' ')
xxx_n=$(awk -F'\t' '$3=="XXX"' /tmp/enriched.tsv | wc -l | tr -d ' ')
stale_n=$(awk -F'\t' -v s="$STALE_DAYS" '$4 > s' /tmp/enriched.tsv | wc -l | tr -d ' ')
linked_n=$(awk -F'\t' '$5 != ""' /tmp/enriched.tsv | wc -l | tr -d ' ')
unlinked_n=$(awk -F'\t' '$5 == ""' /tmp/enriched.tsv | wc -l | tr -d ' ')
# oldest marker
oldest_line=$(awk -F'\t' '$4 >= 0' /tmp/enriched.tsv | sort -t$'\t' -k4 -rn | head -1)
oldest_days=$(echo "$oldest_line" | cut -f4)
oldest_file=$(echo "$oldest_line" | cut -f1)
oldest_lineno=$(echo "$oldest_line" | cut -f2)
oldest_age=$(format_age "$oldest_days")
echo "### Summary"
echo ""
echo "| Metric | Value |"
echo "|:---|---:|"
echo "| Total markers | **${total}** |"
[ "$todo_n" -gt 0 ] && echo "| \`TODO\` | ${todo_n} |"
[ "$fixme_n" -gt 0 ] && echo "| \`FIXME\` | ${fixme_n} |"
[ "$hack_n" -gt 0 ] && echo "| \`HACK\` | ${hack_n} |"
[ "$xxx_n" -gt 0 ] && echo "| \`XXX\` | ${xxx_n} |"
echo "| Files affected | ${files_affected} |"
echo "| Stale (> 6 months) | ${stale_n} |"
echo "| Oldest marker | ${oldest_age}, \`${oldest_file}\` L${oldest_lineno} |"
echo "| Linked to issues | ${linked_n} |"
echo "| Unlinked (no issue ref) | ${unlinked_n} |"
echo ""
# ── by referenced issue ──
if [ -s /tmp/issue_info.tsv ]; then
echo "### By Referenced Issue"
echo ""
has_closed=false
while IFS=$'\t' read -r inum state title; do
count=$(awk -F'\t' -v n="$inum" '$5 ~ "(^|,)"n"(,|$)"' /tmp/enriched.tsv | wc -l | tr -d ' ')
[ "$state" = "closed" ] && has_closed=true
state_icon=""
[ "$state" = "closed" ] && state_icon=" :warning:"
echo "<details><summary><b>#${inum}</b> (${state}): <i>${title}</i> &mdash; ${count} marker(s)${state_icon}</summary>"
echo ""
awk -F'\t' -v n="$inum" '$5 ~ "(^|,)"n"(,|$)"' /tmp/enriched.tsv \
| while IFS=$'\t' read -r f l m d refs txt; do
age_str=$(format_age "$d")
flag=""
[ "$d" -gt "$STALE_DAYS" ] 2>/dev/null && flag=" :warning:"
# re-read the actual source line and trim leading/trailing whitespace
src_text=$(sed -n "${l}p" "$f" 2>/dev/null | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' || true)
echo "- [\`${f}\` L${l}](${REPO_URL}/${f}#L${l}) (${age_str} ago)${flag}"
[ -n "$src_text" ] && echo " \`${src_text}\`"
done
echo ""
echo "</details>"
echo ""
done < /tmp/issue_info.tsv
if [ "$has_closed" = true ]; then
echo "> **Warning:** some markers reference closed issues and may be stale."
echo ""
fi
fi
# ── by file ──
echo "### By File"
echo ""
prev_file=""
prev_lineno=-99
while IFS=$'\t' read -r file lineno marker age_days issues text; do
if [ "$file" != "$prev_file" ]; then
# close previous code block
if [ -n "$prev_file" ]; then
echo '```'
echo ""
fi
file_count=$(awk -F'\t' -v f="$file" '$1==f' /tmp/enriched.tsv | wc -l | tr -d ' ')
echo "#### [\`${file}\`](${REPO_URL}/${file}) &mdash; ${file_count} marker(s)"
echo '```r'
prev_lineno=-99
fi
# blank line between non-sequential lines (visual grouping)
if [ "$file" = "$prev_file" ] && [ $((lineno - prev_lineno)) -gt 1 ]; then
echo ""
fi
age_str=$(format_age "$age_days")
flag=""
[ "$age_days" -gt "$STALE_DAYS" ] 2>/dev/null && flag=" !!"
# re-read the actual source line to avoid TSV round-trip corruption
src_line=$(sed -n "${lineno}p" "$file" 2>/dev/null | sed 's/[[:space:]]*$//' || true)
printf 'L%s: %s ◁ %s ago%s\n' "$lineno" "$src_line" "$age_str" "$flag"
prev_file="$file"
prev_lineno="$lineno"
done < <(sort -t$'\t' -k1,1 -k2,2n /tmp/enriched.tsv)
# close final code block
if [ -n "$prev_file" ]; then
echo '```'
fi
} > todo.md
- name: Update GitHub issue
uses: peter-evans/create-or-update-comment@v4
with:

View File

@@ -1,5 +1,5 @@
Package: AMR
Version: 3.0.1.9065
Version: 3.0.1.9068
Date: 2026-06-24
Title: Antimicrobial Resistance Data Analysis
Description: Functions to simplify and standardise antimicrobial resistance (AMR)

View File

@@ -1,4 +1,4 @@
# AMR 3.0.1.9065
# AMR 3.0.1.9068
Planned as v3.1.0, end of June 2026.

View File

@@ -143,11 +143,10 @@
#' ### Manual additions
#' For convenience, some entries were added manually:
#'
#' - All `r format_included_data_number(length(which(microorganisms$rank == "species group")))` groups and complexes of the [microorganisms.groups] data set, for cross-reference (examples include beta-haemolytic *Streptococcus* groups A to K, coagulase-negative *Staphylococcus* (CoNS), *Mycobacterium tuberculosis* complex, etc.)
#' - `r format_included_data_number(microorganisms[which(microorganisms$source == "manually added" & microorganisms$genus == "Salmonella"), , drop = FALSE])` entries of *Salmonella*, such as the city-like serovars and groups A to H
#' - `r format_included_data_number(length(which(microorganisms$rank == "species group")))` species groups (such as the beta-haemolytic *Streptococcus* groups A to K, coagulase-negative *Staphylococcus* (CoNS), *Mycobacterium tuberculosis* complex, etc.), of which the group compositions are stored in the [microorganisms.groups] data set
#' - 1 entry of *Blastocystis* (*B. hominis*), although it officially does not exist (Noel *et al.* 2005, PMID 15634993)
#' - 1 entry of *Moraxella* (*M. catarrhalis*), which was formally named *Branhamella catarrhalis* (Catlin, 1970) though this change was never accepted within the field of clinical microbiology
#' - 8 other 'undefined' entries (unknown, unknown Gram-negatives, unknown Gram-positives, unknown yeast, unknown fungus, and unknown anaerobic Gram-pos/Gram-neg bacteria)
#' - `r sum(microorganisms$fullname %like% "unknown")` other 'undefined' entries (unknown, unknown Gram-negatives, unknown Gram-positives, unknown yeast, unknown fungus, and unknown anaerobic Gram-pos/Gram-neg bacteria)
#'
#' The syntax used to transform the original data to a cleansed \R format, can be [found here](https://github.com/msberends/AMR/blob/main/data-raw/_reproduction_scripts/reproduction_of_microorganisms.R).
#' @inheritSection AMR Download Our Reference Data

View File

@@ -83,6 +83,7 @@
#' @examples
#' # taxonomic tree -----------------------------------------------------------
#'
#' mo_domain("Klebsiella pneumoniae")
#' mo_kingdom("Klebsiella pneumoniae")
#' mo_phylum("Klebsiella pneumoniae")
#' mo_class("Klebsiella pneumoniae")
@@ -92,6 +93,8 @@
#' mo_species("Klebsiella pneumoniae")
#' mo_subspecies("Klebsiella pneumoniae")
#'
#' # all in one go
#' mo_taxonomy("Klebsiella pneumoniae")
#'
#' # full names and short names -----------------------------------------------
#'
@@ -112,6 +115,7 @@
#' mo_url("Klebsiella pneumoniae")
#' mo_is_yeast(c("Candida", "Trichophyton", "Klebsiella"))
#'
#' mo_group_members("Streptococcus group A")
#' mo_group_members(c(
#' "Streptococcus group A",
#' "Streptococcus group C",
@@ -155,6 +159,7 @@
#'
#' mo_fullname("Staph epidermidis")
#' mo_fullname("Staph epidermidis", Becker = TRUE)
#'
#' mo_shortname("Staph epidermidis")
#' mo_shortname("Staph epidermidis", Becker = TRUE)
#'
@@ -163,6 +168,7 @@
#'
#' mo_fullname("Strep agalactiae")
#' mo_fullname("Strep agalactiae", Lancefield = TRUE)
#'
#' mo_shortname("Strep agalactiae")
#' mo_shortname("Strep agalactiae", Lancefield = TRUE)
#'
@@ -175,10 +181,10 @@
#' mo_gramstain("Klebsiella pneumoniae", language = "el") # Greek
#' mo_gramstain("Klebsiella pneumoniae", language = "uk") # Ukrainian
#'
#' # mo_type is equal to mo_kingdom, but mo_kingdom will remain untranslated
#' mo_kingdom("Klebsiella pneumoniae")
#' # mo_type is equal to mo_domain, but mo_domain will remain untranslated
#' mo_domain("Klebsiella pneumoniae")
#' mo_type("Klebsiella pneumoniae")
#' mo_kingdom("Klebsiella pneumoniae", language = "zh") # Chinese, no effect
#' mo_domain("Klebsiella pneumoniae", language = "zh") # Chinese, no effect
#' mo_type("Klebsiella pneumoniae", language = "zh") # Chinese, translated
#'
#' mo_fullname("S. pyogenes", Lancefield = TRUE, language = "de")
@@ -807,19 +813,19 @@ mo_taxonomy <- function(x, language = get_AMR_locale(), keep_synonyms = getOptio
language <- validate_language(language)
meet_criteria(keep_synonyms, allow_class = "logical", has_length = 1)
x <- as.mo(x, language = language, keep_synonyms = keep_synonyms, ...)
x.mo <- as.mo(x, language = language, keep_synonyms = keep_synonyms, ...)
metadata <- get_mo_uncertainties()
out <- list(
domain = mo_domain(x, language = language, keep_synonyms = keep_synonyms),
kingdom = mo_kingdom(x, language = language, keep_synonyms = keep_synonyms),
phylum = mo_phylum(x, language = language, keep_synonyms = keep_synonyms),
class = mo_class(x, language = language, keep_synonyms = keep_synonyms),
order = mo_order(x, language = language, keep_synonyms = keep_synonyms),
family = mo_family(x, language = language, keep_synonyms = keep_synonyms),
genus = mo_genus(x, language = language, keep_synonyms = keep_synonyms),
species = mo_species(x, language = language, keep_synonyms = keep_synonyms),
subspecies = mo_subspecies(x, language = language, keep_synonyms = keep_synonyms)
domain = mo_domain(x.mo, language = language, keep_synonyms = keep_synonyms),
kingdom = suppressMessages(mo_kingdom(x.mo, language = language, keep_synonyms = keep_synonyms)),
phylum = mo_phylum(x.mo, language = language, keep_synonyms = keep_synonyms),
class = mo_class(x.mo, language = language, keep_synonyms = keep_synonyms),
order = mo_order(x.mo, language = language, keep_synonyms = keep_synonyms),
family = mo_family(x.mo, language = language, keep_synonyms = keep_synonyms),
genus = mo_genus(x.mo, language = language, keep_synonyms = keep_synonyms),
species = mo_species(x.mo, language = language, keep_synonyms = keep_synonyms),
subspecies = mo_subspecies(x.mo, language = language, keep_synonyms = keep_synonyms)
)
load_mo_uncertainties(metadata)

View File

@@ -230,7 +230,7 @@ wisca(example_isolates,
| Piperacillin/tazobactam | Piperacillin/tazobactam + Gentamicin | Piperacillin/tazobactam + Tobramycin |
|:---|:---|:---|
| 70.1% (64.9-75.7%) | 93.6% (92.2-95%) | 89.8% (86.7-92.3%) |
| 69.9% (64.7-75.2%) | 93.7% (92.2-95.1%) | 89.8% (86.8-92.3%) |
WISCA supports stratification by any clinical variable, so you can
generate syndrome-specific or ward-specific coverage estimates:
@@ -245,9 +245,9 @@ wisca(example_isolates,
| Syndromic Group | Piperacillin/tazobactam | Piperacillin/tazobactam + Gentamicin | Piperacillin/tazobactam + Tobramycin |
|:---|:---|:---|:---|
| Clinical | 74.5% (69.3-80.1%) | 93.7% (92-95.1%) | 90.5% (87.1-93.1%) |
| ICU | 56.7% (48-65.5%) | 86.7% (83.4-89.8%) | 82.9% (78.2-87.3%) |
| Outpatient | 57.8% (46.4-69.7%) | 76.5% (70.1-82.2%) | 67.9% (57.9-77.5%) |
| Clinical | 74.6% (69-80.1%) | 93.6% (91.9-95.1%) | 90.5% (86.9-93%) |
| ICU | 57% (48.7-65.8%) | 86.7% (83.7-89.7%) | 82.8% (77.9-87.2%) |
| Outpatient | 57.5% (46.5-68.7%) | 76.7% (70.6-82.4%) | 67.5% (57.2-76.7%) |
**For AMR surveillance**, traditional antibiograms remain the right tool
for tracking resistance per species over time:

View File

@@ -59,11 +59,10 @@ Included taxonomic data from \href{https://lpsn.dsmz.de}{LPSN}, \href{https://ww
For convenience, some entries were added manually:
\itemize{
\item All 37 groups and complexes of the \link{microorganisms.groups} data set, for cross-reference (examples include beta-haemolytic \emph{Streptococcus} groups A to K, coagulase-negative \emph{Staphylococcus} (CoNS), \emph{Mycobacterium tuberculosis} complex, etc.)
\item ~1 500 entries of \emph{Salmonella}, such as the city-like serovars and groups A to H
\item 37 species groups (such as the beta-haemolytic \emph{Streptococcus} groups A to K, coagulase-negative \emph{Staphylococcus} (CoNS), \emph{Mycobacterium tuberculosis} complex, etc.), of which the group compositions are stored in the \link{microorganisms.groups} data set
\item 1 entry of \emph{Blastocystis} (\emph{B. hominis}), although it officially does not exist (Noel \emph{et al.} 2005, PMID 15634993)
\item 1 entry of \emph{Moraxella} (\emph{M. catarrhalis}), which was formally named \emph{Branhamella catarrhalis} (Catlin, 1970) though this change was never accepted within the field of clinical microbiology
\item 8 other 'undefined' entries (unknown, unknown Gram-negatives, unknown Gram-positives, unknown yeast, unknown fungus, and unknown anaerobic Gram-pos/Gram-neg bacteria)
\item 9 other 'undefined' entries (unknown, unknown Gram-negatives, unknown Gram-positives, unknown yeast, unknown fungus, and unknown anaerobic Gram-pos/Gram-neg bacteria)
}
The syntax used to transform the original data to a cleansed \R format, can be \href{https://github.com/msberends/AMR/blob/main/data-raw/_reproduction_scripts/reproduction_of_microorganisms.R}{found here}.

View File

@@ -401,6 +401,7 @@ Visit \href{https://amr-for-r.org/articles/datasets.html}{our website for direct
\examples{
# taxonomic tree -----------------------------------------------------------
mo_domain("Klebsiella pneumoniae")
mo_kingdom("Klebsiella pneumoniae")
mo_phylum("Klebsiella pneumoniae")
mo_class("Klebsiella pneumoniae")
@@ -410,6 +411,8 @@ mo_genus("Klebsiella pneumoniae")
mo_species("Klebsiella pneumoniae")
mo_subspecies("Klebsiella pneumoniae")
# all in one go
mo_taxonomy("Klebsiella pneumoniae")
# full names and short names -----------------------------------------------
@@ -430,6 +433,7 @@ mo_rank("Klebsiella pneumoniae")
mo_url("Klebsiella pneumoniae")
mo_is_yeast(c("Candida", "Trichophyton", "Klebsiella"))
mo_group_members("Streptococcus group A")
mo_group_members(c(
"Streptococcus group A",
"Streptococcus group C",
@@ -473,6 +477,7 @@ mo_shortname("K. pneu rh")
mo_fullname("Staph epidermidis")
mo_fullname("Staph epidermidis", Becker = TRUE)
mo_shortname("Staph epidermidis")
mo_shortname("Staph epidermidis", Becker = TRUE)
@@ -481,6 +486,7 @@ mo_shortname("Staph epidermidis", Becker = TRUE)
mo_fullname("Strep agalactiae")
mo_fullname("Strep agalactiae", Lancefield = TRUE)
mo_shortname("Strep agalactiae")
mo_shortname("Strep agalactiae", Lancefield = TRUE)
@@ -493,10 +499,10 @@ mo_gramstain("Klebsiella pneumoniae", language = "es") # Spanish
mo_gramstain("Klebsiella pneumoniae", language = "el") # Greek
mo_gramstain("Klebsiella pneumoniae", language = "uk") # Ukrainian
# mo_type is equal to mo_kingdom, but mo_kingdom will remain untranslated
mo_kingdom("Klebsiella pneumoniae")
# mo_type is equal to mo_domain, but mo_domain will remain untranslated
mo_domain("Klebsiella pneumoniae")
mo_type("Klebsiella pneumoniae")
mo_kingdom("Klebsiella pneumoniae", language = "zh") # Chinese, no effect
mo_domain("Klebsiella pneumoniae", language = "zh") # Chinese, no effect
mo_type("Klebsiella pneumoniae", language = "zh") # Chinese, translated
mo_fullname("S. pyogenes", Lancefield = TRUE, language = "de")