diff --git a/docker-compose.yml b/docker-compose.yml index 0562d50..9b429ce 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,25 +6,57 @@ services: build: ./nginx restart: always ports: - - 80:80 - deploy: - resources: - limits: - cpus: '0.2' - memory: '0.1g' + - "80:80" + - "443:443" + volumes: + - ./data/certbot/www:/var/www/certbot + - ./data/certbot/conf:/etc/letsencrypt + + certbot: + image: certbot/certbot + container_name: certbot + volumes: + - ./data/certbot/conf:/etc/letsencrypt + - ./data/certbot/www:/var/www/certbot +# # Use the below command to create an initial certificate +# # entrypoint: ["/bin/sh", "-c"] +# # command: ["certbot certonly --webroot --webroot-path=/var/www/certbot --email ENTER_EMAIL_HERE --agree-tos --no-eff-em +# # ail -d ligify.groov.bio --non-interactive"] + entrypoint: ["/bin/sh", "-c", "trap exit TERM; while :; do certbot renew; sleep 12h & wait $${!}; done;"] + ligify: container_name: ligify - build: - ./streamlit + build: ./streamlit restart: always entrypoint: ["streamlit", "run", "entry.py"] ports: - 8501:8501 depends_on: - nginx - deploy: - resources: - limits: - cpus: '1.8' - memory: '0.9g' + - redis + - celery_worker + + redis: + image: redis:5.0.4 + container_name: redis + restart: always + ports: + - "6379:6379" + volumes: + - redis-data:/data + + celery_worker: + container_name: celery_worker + build: + context: ./streamlit + dockerfile: Dockerfile.celery + command: celery -A celery_worker worker --loglevel=info --concurrency=1 + depends_on: + - redis + environment: + - CELERY_BROKER_URL=redis://redis:6379/0 + - CELERY_RESULT_BACKEND=redis://redis:6379/0 + +volumes: + redis-data: \ No newline at end of file diff --git a/nginx/Dockerfile b/nginx/Dockerfile index bf2f691..9b8049b 100644 --- a/nginx/Dockerfile +++ b/nginx/Dockerfile @@ -1,6 +1,17 @@ FROM nginx:latest +# Set environment variable to disable IPv6 listener script +ENV NGINX_DISABLE_IPV6=true + +# Remove the default Nginx configuration file RUN rm /etc/nginx/conf.d/default.conf -COPY default.conf /etc/nginx/conf.d +# Copy custom Nginx configurations +COPY http.conf /etc/nginx/conf.d/http.conf +COPY https.conf /etc/nginx/conf.d/https.conf + +# Create a directory for Certbot webroot +RUN mkdir -p /var/www/certbot +# Expose both the HTTP and HTTPS ports +EXPOSE 80 443 \ No newline at end of file diff --git a/nginx/http.conf b/nginx/http.conf new file mode 100644 index 0000000..8f98067 --- /dev/null +++ b/nginx/http.conf @@ -0,0 +1,14 @@ +server { + listen 80; + server_name ligify.groov.bio; # Change to your actual domain + + location /.well-known/acme-challenge/ { + root /var/www/certbot; + try_files $uri =404; + allow all; + } + + location / { + return 301 https://$host$request_uri; + } +} diff --git a/nginx/default.conf b/nginx/https.conf similarity index 59% rename from nginx/default.conf rename to nginx/https.conf index 639f78c..05ff16c 100644 --- a/nginx/default.conf +++ b/nginx/https.conf @@ -1,20 +1,34 @@ server { + listen 443 ssl; + server_name ligify.groov.bio; # Use your actual server name or domain - listen 80; - server_name ligify_server; + ssl_certificate /etc/letsencrypt/live/ligify.groov.bio/fullchain.pem; + ssl_certificate_key /etc/letsencrypt/live/ligify.groov.bio/privkey.pem; + include /etc/letsencrypt/options-ssl-nginx.conf; + ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; location / { proxy_pass http://ligify:8501/; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; } + location ^~ /static { proxy_pass http://ligify:8501/static/; } + location ^~ /healthz { proxy_pass http://ligify:8501/healthz; } + location ^~ /vendor { proxy_pass http://ligify:8501/vendor; } + location /stream { proxy_pass http://ligify:8501/stream; proxy_http_version 1.1; @@ -24,6 +38,7 @@ server { proxy_set_header Connection "upgrade"; proxy_read_timeout 86400; } + location ^~ /_stcore { proxy_pass http://ligify:8501; proxy_set_header Upgrade $http_upgrade; @@ -32,4 +47,4 @@ server { proxy_set_header Host $host; proxy_cache_bypass $http_upgrade; } -} \ No newline at end of file +} diff --git a/streamlit/.streamlit/config.toml b/streamlit/.streamlit/config.toml new file mode 100644 index 0000000..f829aaf --- /dev/null +++ b/streamlit/.streamlit/config.toml @@ -0,0 +1,186 @@ +# Below are all the sections and options you can have in ~/.streamlit/config.toml. + +[global] + +# By default, Streamlit checks if the Python watchdog module is available and, if not, prints a warning asking for you to install it. The watchdog module is not required, but highly recommended. It improves Streamlit's ability to detect changes to files in your filesystem. +# If you'd like to turn off this warning, set this to True. +# Default: false +disableWatchdogWarning = true + +# If True, will show a warning when you run a Streamlit-enabled script via "python my_script.py". +# Default: true +showWarningOnDirectExecution = false + + +[logger] + +# Level of logging: 'error', 'warning', 'info', or 'debug'. +# Default: 'info' +level = "info" + +# String format for logging messages. If logger.datetimeFormat is set, logger messages will default to `%(asctime)s.%(msecs)03d %(message)s`. See [Python's documentation](https://docs.python.org/2.6/library/logging.html#formatter-objects) for available attributes. +# Default: None +messageFormat = "%(asctime)s %(message)s" + + +[client] + +# Whether to enable st.cache. +# Default: true +caching = true + +# If false, makes your Streamlit script not draw to a Streamlit app. +# Default: true +displayEnabled = true + +# Controls whether uncaught app exceptions are displayed in the browser. By default, this is set to True and Streamlit displays app exceptions and associated tracebacks in the browser. +# If set to False, an exception will result in a generic message being shown in the browser, and exceptions and tracebacks will be printed to the console only. +# Default: true +showErrorDetails = false + + +[runner] + +# Allows you to type a variable or string by itself in a single line of Python code to write it to the app. +# Default: true +magicEnabled = true + +# Install a Python tracer to allow you to stop or pause your script at any point and introspect it. As a side-effect, this slows down your script's execution. +# Default: false +installTracer = false + +# Sets the MPLBACKEND environment variable to Agg inside Streamlit to prevent Python crashing. +# Default: true +fixMatplotlib = true + +[theme] + +base="light" + +# Primary accent for interactive elements +primaryColor = '#5c84fa' + +[server] + +# List of folders that should not be watched for changes. This impacts both "Run on Save" and @st.cache. +# Relative paths will be taken as relative to the current working directory. +# Example: ['/home/user1/env', 'relative/path/to/folder'] +# Default: [] +folderWatchBlacklist = [] + +# Change the type of file watcher used by Streamlit, or turn it off completely. +# Allowed values: * "auto" : Streamlit will attempt to use the watchdog module, and falls back to polling if watchdog is not available. * "watchdog" : Force Streamlit to use the watchdog module. * "poll" : Force Streamlit to always use polling. * "none" : Streamlit will not watch files. +# Default: "auto" +fileWatcherType = "auto" + +# Symmetric key used to produce signed cookies. If deploying on multiple replicas, this should be set to the same value across all replicas to ensure they all share the same secret. +# Default: randomly generated secret key. +cookieSecret = "e95070f37458bc01a700421e511d14f7452df3a1a3f428d434fd127517277629" + +# If false, will attempt to open a browser window on start. +# Default: false unless (1) we are on a Linux box where DISPLAY is unset, or (2) server.liveSave is set. +headless = true + +# Automatically rerun script when the file is modified on disk. +# Default: false +runOnSave = false + +# The address where the server will listen for client and browser connections. Use this if you want to bind the server to a specific address. If set, the server will only be accessible from this address, and not from any aliases (like localhost). +# Default: (unset) +#address = + +# The port where the server will listen for browser connections. +# Default: 8501 +port = 8501 + +# The base path for the URL where Streamlit should be served from. +# Default: "" +baseUrlPath = "" + +# Enables support for Cross-Origin Request Sharing (CORS) protection, for added security. +# Due to conflicts between CORS and XSRF, if `server.enableXsrfProtection` is on and `server.enableCORS` is off at the same time, we will prioritize `server.enableXsrfProtection`. +# Default: true +enableCORS = true + +# Enables support for Cross-Site Request Forgery (XSRF) protection, for added security. +# Due to conflicts between CORS and XSRF, if `server.enableXsrfProtection` is on and `server.enableCORS` is off at the same time, we will prioritize `server.enableXsrfProtection`. +# Default: true +enableXsrfProtection = true + +# Max size, in megabytes, for files uploaded with the file_uploader. +# Default: 200 +maxUploadSize = 1 + +# Enables support for websocket compression. +# Default: true +enableWebsocketCompression = true + + +[browser] + +# Internet address where users should point their browsers in order to connect to the app. Can be IP address or DNS name and path. +# This is used to: - Set the correct URL for CORS and XSRF protection purposes. - Show the URL on the terminal - Open the browser - Tell the browser where to connect to the server when in liveSave mode. +# Default: 'localhost' +serverAddress = "localhost" + +# Whether to send usage statistics to Streamlit. +# Default: true +gatherUsageStats = true + +# Port where users should point their browsers in order to connect to the app. +# This is used to: - Set the correct URL for CORS and XSRF protection purposes. - Show the URL on the terminal - Open the browser - Tell the browser where to connect to the server when in liveSave mode. +# Default: whatever value is set in server.port. +serverPort = 8501 + + +[mapbox] + +# Configure Streamlit to use a custom Mapbox token for elements like st.pydeck_chart and st.map. To get a token for yourself, create an account at https://mapbox.com. It's free (for moderate usage levels)! +# Default: "" +token = "" + + +[deprecation] + +# Set to false to disable the deprecation warning for the file uploader encoding. +# Default: "True" +showfileUploaderEncoding = "True" + +# Set to false to disable the deprecation warning for using the global pyplot instance. +# Default: "True" +showPyplotGlobalUse = "True" + + +[s3] + +# Name of the AWS S3 bucket to save apps. +# Default: (unset) +#bucket = + +# URL root for external view of Streamlit apps. +# Default: (unset) +#url = + +# Access key to write to the S3 bucket. +# Leave unset if you want to use an AWS profile. +# Default: (unset) +#accessKeyId = + +# Secret access key to write to the S3 bucket. +# Leave unset if you want to use an AWS profile. +# Default: (unset) +#secretAccessKey = + +# The "subdirectory" within the S3 bucket where to save apps. +# S3 calls paths "keys" which is why the keyPrefix is like a subdirectory. Use "" to mean the root directory. +# Default: "" +keyPrefix = "" + +# AWS region where the bucket is located, e.g. "us-west-2". +# Default: (unset) +#region = + +# AWS credentials profile to use. +# Leave unset to use your default profile. +# Default: (unset) +#profile = diff --git a/streamlit/Dockerfile b/streamlit/Dockerfile index ec26f3e..57a2a2a 100644 --- a/streamlit/Dockerfile +++ b/streamlit/Dockerfile @@ -1,10 +1,19 @@ +# streamlit/Dockerfile FROM python:3.10 +# Create and set the working directory RUN mkdir /app - WORKDIR /app +# Copy requirements and install dependencies COPY requirements.txt . - RUN pip install -r requirements.txt -COPY . . \ No newline at end of file + +# Copy the entire app +COPY . . + +# Set PYTHONPATH to include both /app and /app/ligify +ENV PYTHONPATH=/app:/app/ligify + +# Expose the default Streamlit port +EXPOSE 8501 diff --git a/streamlit/Dockerfile.celery b/streamlit/Dockerfile.celery new file mode 100644 index 0000000..f2acad7 --- /dev/null +++ b/streamlit/Dockerfile.celery @@ -0,0 +1,16 @@ +# streamlit/Dockerfile.celery +FROM python:3.10 + +# Create and set the working directory +RUN mkdir /app +WORKDIR /app + +# Copy requirements and install dependencies +COPY requirements.txt . +RUN pip install -r requirements.txt + +# Copy the entire app +COPY . . + +# Set PYTHONPATH to include both /app and /app/ligify +ENV PYTHONPATH=/app:/app/ligify diff --git a/streamlit/__init__.py b/streamlit/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/streamlit/celery_worker.py b/streamlit/celery_worker.py new file mode 100644 index 0000000..a76af42 --- /dev/null +++ b/streamlit/celery_worker.py @@ -0,0 +1,22 @@ +# streamlit/celery_worker.py +from celery import Celery +from ligify.fetch_data import fetch_data + +app = Celery('celery_worker', broker='redis://redis:6379/0', backend='redis://redis:6379/0') + +app.conf.update( + result_backend='redis://redis:6379/0', + task_serializer='json', + accept_content=['json'], + result_serializer='json', + timezone='UTC', + enable_utc=True, + worker_concurrency=1, # Ensure only one task is processed at a time +) + +@app.task +def run_ligify_task(chemical, filters): + # Fetch data + regulators, metrics = fetch_data(chemical["InChiKey"], filters) + + return {"regulators": regulators, "metrics": metrics} diff --git a/streamlit/entry.py b/streamlit/entry.py index 2ceab3b..870f372 100644 --- a/streamlit/entry.py +++ b/streamlit/entry.py @@ -1,12 +1,43 @@ +import streamlit as st +from ligify.streamlit_app import run_streamlit, run_ligify +from celery_worker import run_ligify_task, app +import time -import streamlit.web.cli -import sys +def streamlit_run(): + chem, results, prog, chemical, filters = run_streamlit() -from ligify.streamlit_app import run_streamlit, run_ligify + if st.session_state.SUBMITTED: + # Start Celery task + task = run_ligify_task.delay(chemical, filters) + st.session_state.task_id = task.id + st.session_state.SUBMITTED = False + st.experimental_rerun() # Trigger a rerun to avoid immediate execution + if 'task_id' in st.session_state and st.session_state.task_id is not None: + task_id = st.session_state.task_id + task = app.AsyncResult(task_id) -def streamlit_run(): - chem, results, prog, smiles, filters = run_streamlit() - run_ligify(chem, results, prog, smiles, filters) + if task.state == 'SUCCESS': + st.session_state.result = task.result + st.session_state.task_id = None + st.experimental_rerun() # Force rerun to update UI + elif task.state == 'PENDING': + st.write('Task is in the queue, please wait...') + time.sleep(1) # Sleep for a second to avoid too many reruns + st.experimental_rerun() # Check again + elif task.state == 'STARTED': + st.write('Task is currently being processed...') + time.sleep(1) # Sleep for a second to avoid too many reruns + st.experimental_rerun() # Check again + elif task.state == 'FAILURE': + st.write('Task failed, please try again.') + st.session_state.task_id = None + + if 'result' in st.session_state: + regulators = st.session_state.result['regulators'] + metrics = st.session_state.result['metrics'] + + # Update the UI with the results + run_ligify(chem, results, prog, chemical, filters, regulators, metrics) streamlit_run() \ No newline at end of file diff --git a/streamlit/ligify/fetch_data.py b/streamlit/ligify/fetch_data.py index 9c3e21c..d3409e2 100644 --- a/streamlit/ligify/fetch_data.py +++ b/streamlit/ligify/fetch_data.py @@ -18,7 +18,7 @@ def fetch_data(InChiKey, filters): # FETCH REACTIONS - prog_bar = prog.progress(0, text="1. Fetching reaction IDs") + # prog_bar = prog.progress(0, text="1. Fetching reaction IDs") reactions = fetch_reactions(InChiKey = InChiKey, max_reactions = filters["max_reactions"]) total_rxns = len(reactions["rxn_data"]) metrics["RHEA Reactions"] = total_rxns @@ -27,12 +27,12 @@ def fetch_data(InChiKey, filters): # FETCH GENES - prog_bar_increment = 20/int(total_rxns) + # prog_bar_increment = 20/int(total_rxns) counter = 0 for i in reactions["rxn_data"]: - prog_value = int(10+counter*prog_bar_increment) - prog_bar.progress(prog_value, text=f"2. Fetching genes for reaction {str(counter+1)} of {str(total_rxns)} (rhea:{i['rhea_id']})") + # prog_value = int(10+counter*prog_bar_increment) + # prog_bar.progress(prog_value, text=f"2. Fetching genes for reaction {str(counter+1)} of {str(total_rxns)} (rhea:{i['rhea_id']})") associated_proteins = fetch_genes(i["rhea_id"], filters["reviewed"], filters["proteins_per_reaction"]) i["proteins"] = associated_proteins counter += 1 @@ -48,7 +48,7 @@ def fetch_data(InChiKey, filters): if len(reactions["rxn_data"]) == 0: print("No enzymes found") - prog_bar.empty() + # prog_bar.empty() return None, None else: operon_counter = 0 @@ -62,7 +62,7 @@ def fetch_data(InChiKey, filters): if protein["enzyme"]["ncbi_id"] != None: total_genes +=1 - prog_bar_increment = 50/int(total_genes) + # prog_bar_increment = 50/int(total_genes) for rxn in range(0,len(reactions["rxn_data"])): for i in range(0, len(reactions["rxn_data"][rxn]["proteins"])): @@ -72,9 +72,9 @@ def fetch_data(InChiKey, filters): # Limit number of operons evaluated to avoid program taking too long to complete. if operon_counter <= filters["max_operons"]: - prog_value = int(30+operon_counter*prog_bar_increment) + # prog_value = int(30+operon_counter*prog_bar_increment) - prog_bar.progress(prog_value, text=f"3. Fetching operon for gene {str(operon_counter+1)} of {str(total_genes)} ({refseq_id})") + # prog_bar.progress(prog_value, text=f"3. Fetching operon for gene {str(operon_counter+1)} of {str(total_genes)} ({refseq_id})") protein["context"] = acc2operon(refseq_id) operon_counter += 1 @@ -84,7 +84,7 @@ def fetch_data(InChiKey, filters): # FETCH REGULATORS if reactions == None: - prog_bar.empty() + # prog_bar.empty() return None, None else: @@ -92,7 +92,7 @@ def fetch_data(InChiKey, filters): for rxn in reactions["rxn_data"]: for protein in rxn["proteins"]: total_regs += 1 - prog_bar_increment = 20/int(total_regs) + # prog_bar_increment = 20/int(total_regs) # This is where all of the display data is created @@ -102,8 +102,8 @@ def fetch_data(InChiKey, filters): for rxn in reactions["rxn_data"]: for protein in rxn["proteins"]: - prog_value = int(80+counter*prog_bar_increment) - prog_bar.progress(prog_value, text=f"4. Fetching data for regulator {str(counter+1)} of {str(total_regs)} ({protein['organism'][-2]}, {protein['organism'][-1]})") + # prog_value = int(80+counter*prog_bar_increment) + # prog_bar.progress(prog_value, text=f"4. Fetching data for regulator {str(counter+1)} of {str(total_regs)} ({protein['organism'][-2]}, {protein['organism'][-1]})") regs = pull_regulators(protein, rxn) for r in regs: @@ -111,7 +111,7 @@ def fetch_data(InChiKey, filters): counter += 1 metrics["Total regulators"] = len(regulators) - prog_bar.empty() + # prog_bar.empty() @@ -139,6 +139,6 @@ def fetch_data(InChiKey, filters): else: - prog_bar.empty() + # prog_bar.empty() return None, None diff --git a/streamlit/ligify/format_results.py b/streamlit/ligify/format_results.py index 123f74d..ced87c8 100644 --- a/streamlit/ligify/format_results.py +++ b/streamlit/ligify/format_results.py @@ -256,4 +256,4 @@ def bg_color_col(col): c += 1 operon_seq += html st.markdown("
Full operon sequence
", unsafe_allow_html=True) - st.markdown(operon_seq, unsafe_allow_html=True) + st.markdown(operon_seq, unsafe_allow_html=True) diff --git a/streamlit/ligify/predict/accID2operon.py b/streamlit/ligify/predict/accID2operon.py index 1f52e5d..48379e2 100644 --- a/streamlit/ligify/predict/accID2operon.py +++ b/streamlit/ligify/predict/accID2operon.py @@ -1,3 +1,4 @@ +import streamlit as st import requests import re from pprint import pprint @@ -13,61 +14,46 @@ def acc2MetaData(access_id: str): result = requests.get(f"https://eutils.ncbi.nlm.nih.gov/entrez/eutils/efetch.fcgi?db=protein&id={access_id}&rettype=ipg") if result.status_code != 200: - print("non-200 HTTP response. eFetch failed") + st.error("Sorry! NCBI won't send us data due to high traffic. Please try again soon.\ + (metadata eFetch HTTP !== 200)") + return "EMPTY" - parsed = xmltodict.parse(result.text) + else: + parsed = xmltodict.parse(result.text) - if "IPGReport" in parsed["IPGReportSet"].keys(): - if "ProteinList" in parsed["IPGReportSet"]["IPGReport"]: - protein = parsed["IPGReportSet"]["IPGReport"]["ProteinList"]["Protein"] + if "IPGReport" in parsed["IPGReportSet"].keys(): + if "ProteinList" in parsed["IPGReportSet"]["IPGReport"]: + protein = parsed["IPGReportSet"]["IPGReport"]["ProteinList"]["Protein"] - if isinstance(protein, list): - protein = protein[0] + if isinstance(protein, list): + protein = protein[0] - if "CDSList" not in protein.keys(): - return "EMPTY" + if "CDSList" not in protein.keys(): + return "EMPTY" - CDS = protein["CDSList"]["CDS"] + CDS = protein["CDSList"]["CDS"] - #CDS is a list if there is more than 1 CDS returned, otherwise it's a dictionary - if isinstance(CDS, list): - CDS = CDS[0] + #CDS is a list if there is more than 1 CDS returned, otherwise it's a dictionary + if isinstance(CDS, list): + CDS = CDS[0] - proteinDict = { - "accver":CDS["@accver"], - "start":CDS["@start"], - "stop":CDS["@stop"], - "strand":CDS["@strand"], - } + proteinDict = { + "accver":CDS["@accver"], + "start":CDS["@start"], + "stop":CDS["@stop"], + "strand":CDS["@strand"], + } - return proteinDict + return proteinDict + else: + return "EMPTY" else: return "EMPTY" - else: - return "EMPTY" - - - - # OLD VERSION - -# def NC2genome(genome_id, startPos, stopPos): - -# base_url = "https://eutils.ncbi.nlm.nih.gov/entrez/eutils/efetch.fcgi?db=nuccore" -# response = requests.get(base_url+"&id="+str(genome_id)+"&seq_start="+str(startPos)+"&seq_stop="+str(stopPos)+"&rettype=fasta_cds_aa") -# # old script that fetches the entire genome -# # response = requests.get('https://eutils.ncbi.nlm.nih.gov/entrez/eutils/efetch.fcgi?db=nuccore&id='+str(genome_id)+'&rettype=fasta_cds_aa') - -# if response.ok: -# genome = response.text.split("\n") -# return genome - - - def NC2genome(genome_id, operon): base_url = "https://eutils.ncbi.nlm.nih.gov/entrez/eutils/efetch.fcgi?db=nuccore" @@ -158,6 +144,11 @@ def NC2genome(genome_id, operon): return out, genome_reassembly_match + else: + st.error("Sorry! NCBI won't send us data due to high traffic. Please try again soon.\ + (genome eFetch HTTP !== 200)") + return "EMPTY", False + @@ -182,7 +173,9 @@ def getGenes(genome_id, startPos, stopPos): response = requests.get(base_url+"&id="+str(genome_id)+"&seq_start="+str(startPos-5000)+"&seq_stop="+str(stopPos)+"&rettype=fasta_cds_aa") genome = response.text.split("\n") except: - print("error fetching the genome fragment") + st.error("Sorry! NCBI won't send us data due to high traffic. Please try again soon.\ + (genome-fragment eFetch HTTP !== 200)") + return None re1 = re.compile(str(startPos)) @@ -397,7 +390,8 @@ def predict_promoter(operon, regIndex, genome_id): # print('WARNING: Intergenic region is over 800bp') return None else: - print('FATAL: Bad eFetch request') + st.error("Sorry! NCBI won't send us data due to high traffic. Please try again soon.\ + (promoter eFetch HTTP !== 200)") return None # 800bp cutoff for an inter-operon region. diff --git a/streamlit/ligify/streamlit_app.py b/streamlit/ligify/streamlit_app.py index 439efe3..6c2e501 100644 --- a/streamlit/ligify/streamlit_app.py +++ b/streamlit/ligify/streamlit_app.py @@ -13,6 +13,7 @@ def setup_page(): + st.set_page_config(page_title="Ligify", layout='wide', initial_sidebar_state='auto', page_icon="ligify/images/Ligify_Favicon.png") #sys.tracebacklimit = 0 #removes traceback so code is not shown during errors @@ -27,7 +28,8 @@ def setup_page(): st.markdown(f'
Version: {ligify_version}
', unsafe_allow_html=True) - + # removed traceback error messages + st.markdown(""" """, unsafe_allow_html=True) # Removes the full-screen button for various elements @@ -100,6 +102,7 @@ def _connect_form_cb(connect_status): input_mode = head2.radio(label="Select an input mode", options=["SMILES", "Name", "Draw"], horizontal=True) + # OPTIONS options = st.container() col1, col2, col3 = options.columns((1,3,1)) @@ -131,7 +134,8 @@ def _connect_form_cb(connect_status): chemical = {"name": chemical_name, "smiles": smiles, "InChiKey": InChiKey} - + head2.write('Note: Chemical identifiers must be specific. For example, use `D-ribofuranose` rather than `ribose`.\ + Also consider trying an alternative input mode for your chemical.') with st.sidebar: @@ -191,7 +195,7 @@ def _connect_form_cb(connect_status): # SUBMIT BUTTON submit = st.container() - submit_spacer_1, submit_button, submit_spacer_2 = submit.columns([5,1,5]) + submit_spacer_1, submit_button, submit_spacer_2 = submit.columns([5,3,5]) submitted = submit_button.form_submit_button("Submit", use_container_width=True, on_click=_connect_form_cb, args=(True,)) # PROGRESS BAR @@ -217,66 +221,64 @@ def _connect_form_cb(connect_status): # This is essentially the frontend component of the Ligify Web applications. -def run_ligify(chem, results, progress, chemical, filters): +def run_ligify(chem, results, progress, chemical, filters, regulators, metrics): m_spacer1, metrics_col, m_spacer2 = results.container().columns((1,3,1)) regulator_column, data_column = results.columns([1,3]) - if st.session_state.SUBMITTED: - - if chemical["smiles"] == None: + if chemical["smiles"] == None: data_column.subheader("Chemical input was not recognized. Please try a different input method.") + else: + + # SMILES = str(chemical_smiles) + # chem.image(f'http://hulab.rxnfinder.org/smi2img/{SMILES}/', width=200) + + # if not regulators and not metrics: + # regulators, metrics = fetch_data(chemical["InChiKey"], filters) + + select_spacerL, please_select, select_spacerR = data_column.container().columns([1,2,1]) + + format_results(data_column, chemical["name"]) + + regulator_column.header('') + regulator_column.subheader('Sensor candidates') + regulator_column.divider() + + # If no regulators are returned, suggest alternative queries + if regulators == None: + similar_chemicals = blast_chemical(chemical["smiles"], filters["max_alt_chems"]) + regulator_column.write("No regulators found") + please_select.subheader("No associated reactions :pensive:") + please_select.write("Consider an alternative query") + data_column.dataframe(similar_chemicals) + + # If regulators are returned, format display else: - # SMILES = str(chemical_smiles) - # chem.image(f'http://hulab.rxnfinder.org/smi2img/{SMILES}/', width=200) - - regulators, metrics = fetch_data(chemical["InChiKey"], filters) - - select_spacerL, please_select, select_spacerR = data_column.container().columns([1,2,1]) - - format_results(data_column, chemical["name"]) - - regulator_column.header('') - regulator_column.subheader('Sensor candidates') - regulator_column.divider() - - # If no regulators are returned, suggest alternative queries - if regulators == None: - similar_chemicals = blast_chemical(chemical["smiles"], filters["max_alt_chems"]) - regulator_column.write("No regulators found") - please_select.subheader("No associated reactions :pensive:") - please_select.write("Consider an alternative query") - data_column.dataframe(similar_chemicals) - - # If regulators are returned, format display - else: - - # Metrics data - metrics_col.subheader("Search metrics") - m_rhea, m_genes, m_filtered, m_operons, m_regs = metrics_col.columns(5) - m_rhea.metric("Rhea reactions", metrics["RHEA Reactions"]) - m_genes.metric("Bacterial genes", metrics["Total genes"]) - m_filtered.metric("Filtered genes", metrics["Filtered genes"]) - m_operons.metric("Operons", metrics["Total operons"]) - m_regs.metric("Regulators", metrics["Total regulators"]) - metrics_col.divider() - - if not st.session_state.data: - please_select.subheader("Please select a regulator") - - reg_acc_col, rank_col = regulator_column.columns((2,1)) - reg_acc_col.markdown("
Regulator
", unsafe_allow_html=True) - rank_col.markdown("
Rank
", unsafe_allow_html=True) - - for i in range(0, len(regulators)): - name = "var"+str(i) - rank = regulators[i]["rank"]["rank"] - color = regulators[i]["rank"]["color"] - rank_col.markdown(f"

{rank}

", unsafe_allow_html=True) - name = reg_acc_col.form_submit_button(regulators[i]['refseq']) - if name: - st.session_state.data = regulators[i] - st.experimental_rerun() - + # Metrics data + metrics_col.subheader("Search metrics") + m_rhea, m_genes, m_filtered, m_operons, m_regs = metrics_col.columns(5) + m_rhea.metric("Rhea reactions", metrics["RHEA Reactions"]) + m_genes.metric("Bacterial genes", metrics["Total genes"]) + m_filtered.metric("Filtered genes", metrics["Filtered genes"]) + m_operons.metric("Operons", metrics["Total operons"]) + m_regs.metric("Regulators", metrics["Total regulators"]) + metrics_col.divider() + + if not st.session_state.data: + please_select.subheader("Please select a regulator") + + reg_acc_col, rank_col = regulator_column.columns((2,1)) + reg_acc_col.markdown("
Regulator
", unsafe_allow_html=True) + rank_col.markdown("
Rank
", unsafe_allow_html=True) + + for i in range(0, len(regulators)): + name = "var"+str(i) + rank = regulators[i]["rank"]["rank"] + color = regulators[i]["rank"]["color"] + rank_col.markdown(f"

{rank}

", unsafe_allow_html=True) + name = reg_acc_col.form_submit_button(regulators[i]['refseq']) + if name: + st.session_state.data = regulators[i] + st.experimental_rerun() diff --git a/streamlit/requirements.txt b/streamlit/requirements.txt index 8f0a149..ff242dc 100644 --- a/streamlit/requirements.txt +++ b/streamlit/requirements.txt @@ -10,4 +10,6 @@ streamlit-ketcher==0.0.1 rdkit==2023.3.2 libChEBIpy==1.0.10 biopython==1.81 -dnachisel==3.2.11 \ No newline at end of file +dnachisel==3.2.11 +redis==5.0.4 +celery==5.4.0 \ No newline at end of file