import os import re import shutil import tempfile import base64 from io import StringIO from Bio import Entrez, SeqIO from Bio.SeqFeature import SeqFeature, FeatureLocation Entrez.email = "user@example.com" def fetch_genbank(accession, out_dir): handle = Entrez.efetch(db="nuccore", id=accession, rettype="gbwithparts", retmode="text") record = SeqIO.read(handle, "genbank") handle.close() path = os.path.join(out_dir, f"{accession}.gbk") SeqIO.write(record, path, "genbank") return path def fasta_to_gbk(fasta_text, out_dir): records = list(SeqIO.parse(StringIO(fasta_text), "fasta")) if not records: return [], ["Could not parse any sequences from the FASTA input."] try: import pyrodigal orf_finder = pyrodigal.GeneFinder(meta=True) except ImportError: orf_finder = None gbk_files, warnings = [], [] for record in records: record.annotations["molecule_type"] = "DNA" if orf_finder is not None: try: genes = orf_finder.find_genes(str(record.seq).encode()) for i, gene in enumerate(genes, 1): record.features.append( SeqFeature( FeatureLocation(gene.begin - 1, gene.end, strand=gene.strand), type="CDS", qualifiers={"locus_tag": [f"{record.id}_{i:04d}"]}, ) ) if not list(genes): warnings.append(f"{record.id}: no ORFs predicted (sequence may be too short).") except Exception as e: warnings.append(f"{record.id}: ORF prediction failed — {e}") else: warnings.append(f"{record.id}: pyrodigal not installed, saving without gene annotation.") safe_id = re.sub(r"[^\w\-]", "_", record.id) path = os.path.join(out_dir, f"{safe_id}.gbk") SeqIO.write(record, path, "genbank") gbk_files.append(path) return gbk_files, warnings def _wrap_in_iframe(html_content): html_b64 = base64.b64encode(html_content.encode("utf-8")).decode("ascii") return ( f'' ) def _error_html(msg): return f'
{msg}
' def run_clinker(accessions_text, fasta_text=""): accessions = [a.strip() for a in accessions_text.strip().splitlines() if a.strip()] has_fasta = bool(fasta_text.strip()) if not accessions and not has_fasta: return _error_html("Please enter at least one accession or a FASTA sequence.") work_dir = tempfile.mkdtemp() gbk_files, errors = [], [] for acc in accessions: try: gbk_files.append(fetch_genbank(acc, work_dir)) except Exception as e: errors.append(f"{acc}: {e}") if has_fasta: fasta_gbks, fasta_warnings = fasta_to_gbk(fasta_text, work_dir) gbk_files.extend(fasta_gbks) errors.extend(fasta_warnings) if not gbk_files: shutil.rmtree(work_dir) return _error_html("No usable input files.