File size: 4,239 Bytes
e77adef
99df945
e77adef
 
6405de8
99df945
e77adef
99df945
e77adef
 
 
 
 
 
 
 
 
 
 
 
 
99df945
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
6405de8
 
 
 
 
 
 
 
99df945
 
 
 
 
e77adef
99df945
 
 
 
e77adef
 
99df945
e77adef
 
 
 
 
 
 
99df945
 
 
 
 
e77adef
 
99df945
e77adef
6405de8
 
 
 
 
 
 
 
db16b6b
6405de8
 
 
 
 
 
99df945
e77adef
6405de8
e77adef
99df945
e77adef
6405de8
e77adef
 
 
99df945
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
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'<iframe src="data:text/html;base64,{html_b64}"'
        ' style="width:100%;height:800px;border:0;" allowfullscreen></iframe>'
    )


def _error_html(msg):
    return f'<p style="color:#c0392b;font-family:sans-serif;padding:1rem">{msg}</p>'


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.<br>" + "<br>".join(errors))

    html_path = os.path.join(work_dir, "output.html")
    try:
        try:
            from clinker.main import clinker as clinker_api
            clinker_api(gbk_files, plot=html_path, force=True)
        except ImportError:
            import subprocess
            r = subprocess.run(
                ["clinker", "-f", "-i", "0.225", "-p", html_path] + gbk_files,
                capture_output=True, text=True,
            )
            if r.returncode != 0 and not os.path.exists(html_path):
                raise RuntimeError(r.stderr)
    except Exception as e:
        shutil.rmtree(work_dir)
        return _error_html(f"clinker error: {e}")

    if not os.path.exists(html_path):
        shutil.rmtree(work_dir)
        return _error_html("clinker ran but produced no HTML output.")

    with open(html_path, encoding="utf-8") as f:
        html_content = f.read()

    shutil.rmtree(work_dir)
    return _wrap_in_iframe(html_content)