File size: 9,811 Bytes
a96bcc0
 
 
 
 
82059dc
b58e62c
a96bcc0
73c143d
 
 
 
e181215
 
 
 
a96bcc0
 
 
b58e62c
 
cb0e74e
 
a96bcc0
 
efa4d73
 
82059dc
73c143d
efa4d73
046aaba
a96bcc0
82059dc
a96bcc0
 
 
 
 
efa4d73
55788de
 
 
 
 
 
efa4d73
 
 
 
55788de
 
9c013b0
efa4d73
a96bcc0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a17e473
 
 
 
 
a96bcc0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
046aaba
a96bcc0
 
 
 
 
 
 
 
 
 
046aaba
a96bcc0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
046aaba
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
73c143d
a96bcc0
 
 
 
 
 
 
 
 
 
 
 
60a24bb
 
 
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
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
import io, uuid, time, os
import flask
from flask_socketio import SocketIO, emit, join_room, leave_room
import eventlet
from core.mtdna_backend import *
from werkzeug.middleware.proxy_fix import ProxyFix
import mimetypes

# Many things running at once on HuggingFace and sockets so money_patch makes codes that looks synchronous run cooperatively
# == resolve incomplete responses for static files caused by socket didnt cooperate with Eventletl's loop
eventlet.monkey_patch()

print("CWD:", os.getcwd())
print("templates/ exists:", os.path.isdir("templates"), "->", os.listdir("templates") if os.path.isdir("templates") else "missing")
print("static/ exists:", os.path.isdir("static"), "->", os.listdir("static")[:10] if os.path.isdir("static") else "missing")

# accessions = []
isvip = True  # or True depending on the user

mimetypes.add_type("text/css", ".css") 

BASE_DIR = os.path.dirname(os.path.abspath(__file__))
app = flask.Flask(__name__, static_folder=os.path.join(BASE_DIR, "static"), static_url_path="/static", template_folder=os.path.join(BASE_DIR, "templates"))
app.config["DEBUG"] = True
app.config["SECRET_KEY"] = "dev-key"

# important behind HF proxy
app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_prefix=1)

# # Socket.IO (logs on for debugging)
socketio = SocketIO(app, async_mode="eventlet", cors_allowed_origins="*")


# --- job registry for cancel flags ---
# Use a simple boolean flag in eventlet mode: True => cancel requested
CANCEL_FLAGS = {}  # {job_id: bool}
JOBS = {} # {job_id: {"accs": [...], "started": False}}

# (optional) no-cache while debugging
# @app.after_request
# def nocache(resp):
#     resp.headers["Cache-Control"] = "no-store"
#     return resp

print(app.url_map)

# request log to verify what the proxy is asking for
@app.before_request
def _log():
    if flask.request.path.startswith('/static/') or flask.request.path == '/favicon.ico' or flask.request.path.startswith('/socket.io'):
        return
    print("REQ:", flask.request.method, flask.request.path)

# Home
@app.route("/")
def home():
    return flask.render_template("Home.html", isvip=isvip)

# Submit route
@app.route("/submit", methods=["POST"])
def submit():
    raw_text = flask.request.form.get("raw_text", "").strip()
    file_upload = flask.request.files.get("file_upload")
    user_email = flask.request.form.get("user_email", "").strip()

    if file_upload and getattr(file_upload, "filename", ""):
        data = file_upload.read()
        buf = io.BytesIO(data); buf.name = file_upload.filename
        file_upload = buf

    accs, error = extract_accessions_from_input(file=file_upload, raw_text=raw_text)

    job_id = uuid.uuid4().hex[:8]
    CANCEL_FLAGS[job_id] = False

    # Obtain user's past usage
    user_hash = hash_user_id(user_email)
    user_usage, max_allowed = increment_usage(user_hash, 0) # get how much they have run and the maximun #queries they have
    remaining_trials = max(max_allowed - user_usage, 0) # remaining trials if everything goes well
    total_queries = max(0, min(len(accs), max_allowed - user_usage)) # limited the number of queries of users so that won't have to run all.
    
    # the list of IDs that will be run within allowance
    accs = accs[:total_queries]

    # Save var to the global environment
    JOBS[job_id] = {"accs": accs, 
                    "user": {
                        "user_hash": user_hash, 
                        "user_usage": user_usage, 
                        "max_allowed": max_allowed,
                        "total_queries": total_queries,
                        "remaining_trials": remaining_trials
                        }, 
                    "started": False} 

    return flask.redirect(flask.url_for("output", job_id=job_id), code=303)

@app.route("/output/")
def output_root():
    return flask.redirect(flask.url_for("home"))

# Output page (must accept job_id!)
@app.route("/output/<job_id>")
def output(job_id):
    started_at = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
    total_queries = JOBS[job_id]['user']['total_queries']
    return flask.render_template(
        "Output.html",
        job_id=job_id,
        started_at=started_at,
        total_queries=total_queries,
        isvip=isvip,
        ws_url="",  # leave empty for mock/demo mode; set later to real WS URL
    )

# Functions that communicates between web and server - run via socketio
def run_job(job_id, accessions):
    total_queries = JOBS[job_id]['user']['total_queries']
    user_hash = JOBS[job_id]['user']['user_hash'] # to update allowance in case cancelled
    room = job_id


    def send_log(msg): socketio.emit("log", {"msg": msg}, room=room)
    def send_row(row): socketio.emit("row", row, room=room)

    
    try:
        socketio.emit("status", {"state": "started", "total": len(accessions)}, room=room)
        start_time = time.perf_counter()
        send_log(f"Job {job_id} started. {total_queries} accession(s).")

        outs = []
        for i, acc in enumerate(accessions, 1):
            if CANCEL_FLAGS.get(job_id):
                send_log("Cancellation requested. Stopping…")
                socketio.emit("status", {"state": "cancelled"}, room=room)
                increment_usage(user_hash, i - 1)
                return

            t0 = time.perf_counter()
            out = summarize_results(acc)  # may be dict / [] / None
            dt = time.perf_counter() - t0

            # ---- normalise 'out' to a dict we can emit safely ----
            if not out:
                out = {}
            elif isinstance(out, list):
                # If a list slipped through, try to coerce sensibly
                if out and isinstance(out[0], dict):
                    out = out[0]
                elif out and isinstance(out[0], list):
                    # very defensive: list-of-lists -> map by expected order
                    keys = [
                        "Sample ID", "Predicted Country", "Country Explanation",
                        "Predicted Sample Type", "Sample Type Explanation",
                        "Sources", "Time cost"
                    ]
                    row0 = out[0]
                    out = {k: (row0[idx] if idx < len(row0) else "") for idx, k in enumerate(keys)}
                else:
                    out = {}
            elif not isinstance(out, dict):
                out = {}

            # ---- map backend keys (Title Case) to frontend keys (snake_case) ----
            sample_id                 = out.get("Sample ID") or str(acc)
            predicted_country         = out.get("Predicted Country", "unknown")
            country_explanation       = out.get("Country Explanation", "unknown")
            predicted_sample_type     = out.get("Predicted Sample Type", "unknown")
            sample_type_explanation   = out.get("Sample Type Explanation", "unknown")
            sources                   = out.get("Sources", "No Links")
            time_cost                 = out.get("Time cost") or f"{dt:.2f}s"

            send = {
                "idx": i,
                "sample_id": sample_id,
                "predicted_country": predicted_country,
                "country_explanation": country_explanation,
                "predicted_sample_type": predicted_sample_type,
                "sample_type_explanation": sample_type_explanation,
                "sources": sources,
                "time_cost": time_cost,
            }

            socketio.emit("row", send, room=room)
            socketio.sleep(0)  # <- correct spelling; yield so the emit flushes
            send_log(f"Processed {acc} in {dt:.2f}s")

        total_dt = time.perf_counter() - start_time
        
        # Update user allowance
        increment_usage(user_hash, total_queries)
        # Calculate remaining_trials to display for user
        remaining_trials = JOBS[job_id]['user']['remaining_trials']

        socketio.emit("status", {"state": "finished", "elapsed": f"{total_dt:.2f}s"}, room=room)
        send_log(f"Job finished successfully. Number of trials left is")
    except Exception as e:
        send_log(f"ERROR: {e}")
        socketio.emit("status", {"state": "error", "message": str(e)}, room=room)
    finally:
        CANCEL_FLAGS.pop(job_id, None)
        JOBS.pop(job_id, None)  # <— tidy queued job

# ---- Socket.IO events ----
@socketio.on("connect")
def on_connect():
    emit("connected", {"ok": True})

@socketio.on("join")
def on_join(data):
    job_id = data.get("job_id")
    if job_id:
        join_room(job_id)
        emit("joined", {"room": job_id})

        # Start the job once the client is in the room
        job = JOBS.get(job_id)
        if job and not job["started"]:
            job["started"] = True
            total = len(job["accs"])
            # Send an initial queued/total status so the UI can set progress denominator
            socketio.emit("status", {"state": "queued", "total": total}, room=job_id)
            socketio.start_background_task(run_job, job_id, job["accs"])

@socketio.on("leave")
def on_leave(data):
    job_id = data.get("job_id")
    if job_id:
        leave_room(job_id)

@socketio.on("cancel")
def on_cancel(data):
    job_id = data.get("job_id")
    if job_id in CANCEL_FLAGS:
        CANCEL_FLAGS[job_id] = True  # flip the flag
        emit("status", {"state": "cancelling"}, room=job_id)

@app.route("/about")
def about():
    return flask.render_template("About.html", isvip=isvip)

@app.route("/pricing")
def pricing():
    return flask.render_template("Pricing.html", isvip=isvip)

@app.route("/contact")
def contact():
    return flask.render_template("Contact.html", isvip=isvip)

if __name__ == "__main__":
    port = int(os.environ.get("PORT", 7860))  # HF Spaces injects PORT
    socketio.run(app, host="0.0.0.0", port=port)