5afbafb6dde0b144cca0fe3f377a5b284e3561d3
max
  Mon Sep 14 10:26:17 2026 -0700
uniprot otto: an organism whose proteins do not align should not stop the run

aquChr2, the golden eagle, took the run down with a bare

AssertionError:

on assert(os.path.getsize(mapFname)!=0). UniProt has two proteins for taxon 216574
and neither aligned, so the mapping PSL came out empty. That is a fact about the
data rather than a failure of the pipeline, and it is the same situation as an
organism with no proteins at all, which is already skipped with a warning. Several
more taxa in the plan are in the same position, with 1, 2, 8, 13 and 17 proteins.

The assembly is now skipped with a message naming it, both when the mapping was
just built and when an empty one is being reused from an earlier run.

Worker failures also log a traceback now. errAbort raises a bare AssertionError, so
the pool reported exactly "taxon 216574 failed: AssertionError:" and said nothing
about where, which is worse than what the sequential path used to print. Checked
that the errAbort message, the traceback and the file and line all appear.

refs #38300

diff --git src/hg/utils/otto/uniprot/doUniprot src/hg/utils/otto/uniprot/doUniprot
index c2858fca1a0..96f1d9d05ad 100755
--- src/hg/utils/otto/uniprot/doUniprot
+++ src/hg/utils/otto/uniprot/doUniprot
@@ -1,20 +1,20 @@
 #!/usr/bin/env python3
 import urllib.request, urllib.error, urllib.parse, time, os, atexit
 import datetime, optparse, sys, logging, subprocess, glob, shutil
 import sys, gzip, logging,re, json
-import concurrent.futures
+import concurrent.futures, traceback
 
 from urllib.parse import urlparse
 from os.path import *
 from os import system, makedirs, mkdir, remove, listdir
 from shutil import move
 from subprocess import PIPE
 from collections import defaultdict, namedtuple, Counter, OrderedDict
 
 # main driver script for uniprot updates
 
 # This script has evolved over a long time. Parts of it do not have an optimal structure.
 # One reason is that the script used to parse only mutations. Even normal annotations
 # are still put into a structure that resembles mutations.
 
 # the script first uses makeUniProtPsl.sh to create a mapping from UniProt Sequence to genome as a PSL file.
@@ -2149,31 +2149,39 @@
         stats["minAli"] = MINALI
         workDir = "clusterRun-map-%s-%s-%s.tmp" % (db, geneTable, fullMd5)
 
         scriptArgs = [protFa, transcriptFa, transcriptPsl, str(MINALI), workDir, mapFname]
         if selectFname is not None:
             scriptArgs.append(selectFname)
 
         # This is where the BLAST alignment-meat happens
         cmd = ["time", "./makeUniProtPsl.sh"]
         cmd.extend(scriptArgs)
         logging.info("Running %s" % cmd)
         run(cmd)
 
     writeMapDesc(stats, db, geneTable, mapDescFname)
 
-    assert(os.path.getsize(mapFname)!=0)
+    if os.path.getsize(mapFname)==0:
+        # Nothing aligned. On an organism with a handful of UniProt proteins this is a fact
+        # about the data, not a failure of the run: taxon 216574, the golden eagle, offers
+        # two proteins and neither of them aligns. Treat it the way an organism with no
+        # proteins at all is already treated, skip the assembly and carry on, rather than
+        # asserting and taking the whole run down. refs #38300
+        logging.warning("%s: no protein aligned to the genome, skipping this assembly" % db)
+        return None, geneTable, fullMd5, protMapSource, protToTrans, True
+
     # if you modify this line, also modify the other return statement above
     return mapFname, geneTable, fullMd5, protMapSource, protToTrans, True
 
 def convMapToBigPsl(fastaFname, mapFname, bigPslDir, accToDb, accToMeta, accToMapSource, \
         accToTrans, chromSizesFname, doTrembl):
     " convert the .psl mapping file to bigPsl annotated with a few basic prot infos "
     bigPslSwissFname = join(bigPslDir, "unipAliSwissprot.new.bb" % locals())
 
     if doTrembl:
         bigPslTremblFname = join(bigPslDir, "unipAliTrembl.new.bb" % locals())
     else:
         bigPslTremblFname = None
 
     protSeqs = parseFasta(open(fastaFname))
     infoFname = join(bigPslDir, "protMapInfo.tsv")
@@ -2375,30 +2383,35 @@
             if onlyDbs is not None and db not in onlyDbs:
                 continue
 
             logging.debug("Annotating assembly %s" % db)
             # a GenArk assembly has no /hive/data/genomes directory, its chrom.sizes
             # lives in the hub
             chromSizesFname = chromSizesFile(db)
 
             accToMeta = parseRecordMeta(tabDir, taxId, doTrembl)
 
             dbMapDir = makeSubDir(mapDir, db)
             mapDescFname = join(dbMapDir, "liftInfo.json")
             mapFname, geneTable, mapMd5, accToMapSource, accToTrans, isNewMap = \
                 makeProteinGenomePsl(taxId, fullFaFname, tabFnames, db, options.force, dbMapDir, mapDescFname)
 
+            if mapFname is None or os.path.getsize(mapFname)==0:
+                # covers both the freshly built and the reused mapping file
+                logging.warning("%s: no protein/genome alignments, nothing to build" % db)
+                continue
+
             dbBigBedDir = makeSubDir(bigBedDir, db)
 
             convMapToBigPsl(fullFaFname, mapFname, dbBigBedDir, accToDb, accToMeta, accToMapSource, accToTrans, chromSizesFname, doTrembl)
 
             #shutil.copy(mapDescFname, mapDir)
 
             if options.onlyMap:
                 continue
 
             annotFnames = ["tab/swissprot.%d.annots.tab" % taxId]
             if doTrembl:
                 annotFnames.append( "tab/trembl.%d.annots.tab" % taxId )
 
             uniprotLift(fullFaFname, annotFnames, chromSizesFname, mapFname, dbBigBedDir, accToDb, accToMeta, options)
 
@@ -2430,31 +2443,34 @@
     with concurrent.futures.ThreadPoolExecutor(max_workers=options.taxonThreads) as pool:
         futures = {}
         for taxId, dbs in todo:
             fut = pool.submit(oneTaxon, taxId, dbs, onlyDbs, options, tabDir, faDir,
                     mapDir, bigBedDir, doTrembl)
             futures[fut] = taxId
         for fut in concurrent.futures.as_completed(futures):
             taxId = futures[fut]
             try:
                 fut.result()
             except BaseException as ex:
                 # BaseException, not Exception: run() reports a failed command with
                 # sys.exit(), which raises SystemExit. In a worker thread that would
                 # otherwise kill just that thread and leave the run looking successful,
                 # which is exactly the kind of silent failure this pipeline has a history of.
-                logging.error("taxon %s failed: %s: %s" % (taxId, type(ex).__name__, ex))
+                # Log the traceback too: errAbort raises a bare AssertionError, so without
+                # one the message is just "AssertionError:" and says nothing about where.
+                logging.error("taxon %s failed: %s: %s\n%s" % (taxId, type(ex).__name__, ex,
+                        "".join(traceback.format_exception(type(ex), ex, ex.__traceback__))))
                 failed.append(taxId)
 
     if failed:
         errAbort("%d of %d taxa failed: %s" % (len(failed), len(todo), ", ".join(str(t) for t in sorted(failed))))
 
 def makeSymlink(target, linkName):
     assert(".new" not in linkName) # make sure that this bug never happens again
 
     if target is None:
         logging.error("internal error, but not stopping: Symlink to a None target?")
         return
 
     targetPath = abspath(target)
 
     if not isfile(targetPath):