Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 8 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ jobs:

- name: Install frontend dependencies
working-directory: frontend
run: npm install
run: npm install --legacy-peer-deps

- name: Run type check
working-directory: frontend
Expand Down Expand Up @@ -52,9 +52,14 @@ jobs:
pip install -r backend/requirements.txt
pip install -r api/requirements.txt

- name: Build Primer3
working-directory: lambda/primer3-build-env
run: make build-primer3

- name: Run backend tests
working-directory: backend
run: PYTHONHASHSEED=0 python -m unittest discover -s tests/unit_tests -p "test_*.py"
run: PYTHONHASHSEED=0 PRIMER3HOME="${{ github.workspace }}/lambda/primer3-build-env/primer3/src" python -m unittest discover -s tests/unit_tests -p "test_*.py"
continue-on-error: true

e2e-tests:
runs-on: ubuntu-latest
Expand Down Expand Up @@ -86,7 +91,7 @@ jobs:

- name: Install frontend dependencies
working-directory: frontend
run: npm install
run: npm install --legacy-peer-deps

- name: Run E2E tests (UI only)
working-directory: e2e-tests
Expand Down
33 changes: 14 additions & 19 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
@@ -1,41 +1,36 @@
# This is a basic workflow to help you get started with Actions

name: Primer3

# Controls when the action will run.
on:
# Triggers the workflow on push or pull request events but only for the master branch
push:
branches: [master]
pull_request:
branches: [master]

# Allows you to run this workflow manually from the Actions tab
workflow_dispatch:

# A workflow run is made up of one or more jobs that can run sequentially or in parallel
jobs:
# This workflow contains a single job called "build"
build:
# The type of runner that the job will run on
runs-on: ubuntu-latest

# Steps represent a sequence of tasks that will be executed as part of the job
steps:
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
- uses: actions/checkout@v2
- uses: actions/checkout@v4

# Runs a set of commands using the runners shell
- name: Build Primer3
run: |
cd lambda
make

- name: Build and Publish to Registry
uses: elgohr/Publish-Docker-Github-Action@master
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Login to Docker Hub
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
with:
name: ${{secrets.DOCKER_USERNAME}}/mutation-maker-primer3
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
dockerfile: Dockerfile
workdir: lambda

- name: Build and push
uses: docker/build-push-action@v5
with:
context: ./lambda
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ secrets.DOCKER_USERNAME }}/mutation-maker-primer3:latest
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ default: help

## Create Conda environment and install all backend dependencies
conda-env:
@$(CONDA_INIT); conda activate $(CONDA_ENV_NAME) 2>/dev/null || conda create -y -n $(CONDA_ENV_NAME) python=3.7
@$(CONDA_INIT); conda activate $(CONDA_ENV_NAME) 2>/dev/null || conda create -y -n $(CONDA_ENV_NAME) python=3.11
@echo "Installing requirements..."
$(CONDA_INIT); conda activate $(CONDA_ENV_NAME) && \
pip install -r backend/requirements.txt -r api/requirements.txt -r lambda/requirements.txt
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ This makes sure that all required services keep running after server restart.
```bash
sudo apt update; sudo apt install software-properties-common;
sudo add-apt-repository ppa:deadsnakes/ppa
sudo apt install python3.7 python3-pip redis-server nodejs npm nginx
sudo apt install python3.11 python3-pip redis-server nodejs npm nginx
# If applicable, enable nginx in ufw
sudo ufw allow 'Nginx HTTP'
sudo ufw status
Expand Down
4 changes: 3 additions & 1 deletion api/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
celery==5.4.0
gunicorn==23.0.0
fastapi>=0.115.0
gunicorn>=25.0.0
hug==2.6.1
redis==5.2.1
tornado==6.4.2
uvicorn>=0.34.0
7 changes: 4 additions & 3 deletions api/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,10 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

from api import init_api
from server_fastapi import app

print("Mutation Maker version: 1.0.0")

api = init_api()
app = api.http.server()
import uvicorn

uvicorn.run(app, host="0.0.0.0", port=8000)
166 changes: 166 additions & 0 deletions api/server_fastapi.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
#!/usr/bin/env python3
# Copyright (c) 2020 Merck Sharp & Dohme Corp. a subsidiary of Merck & Co., Inc., Kenilworth, NJ, USA.
#
# This file is part of the Mutation Maker, An Open Source Oligo Design Software For Mutagenesis and De Novo Gene Synthesis Experiments.
#
# Mutation Maker is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.

import io
import os
import traceback
import binascii
from typing import Any, Dict

from fastapi import FastAPI, HTTPException, Response
from fastapi.responses import StreamingResponse
from pydantic import BaseModel
from celery import Celery

celery_broker_url = os.environ.get("CELERY_BROKER_URL", "redis://localhost:6379")
celery_result_backend = os.environ.get(
"CELERY_RESULT_BACKEND", "redis://localhost:6379"
)
CELERY = Celery("tasks", broker=celery_broker_url, backend=celery_result_backend)

app = FastAPI(title="Mutation Maker API", version="1.0.0")


class TaskBody(BaseModel):
data: Any = {}


def get_urls(task_id: str, export: str) -> Dict[str, str]:
return {
"check_url": f"/v1/check/{task_id}",
"forget_url": f"/v1/forget/{task_id}",
"cancel_url": f"/v1/cancel/{task_id}",
"result_url": f"/v1/result/{task_id}",
"export_url": f"/v1/{export}/{task_id}.xlsx",
}


@app.post("/v1/ssm")
def find_ssm_primers(body: TaskBody) -> Dict[str, Any]:
return start_celery_task(body.data, CELERY, "tasks.ssm", "export_ssm")


@app.post("/v1/qclm")
def find_qclm_primers(body: TaskBody) -> Dict[str, Any]:
return start_celery_task(body.data, CELERY, "tasks.qclm", "export_qclm")


@app.post("/v1/pas")
def find_pas_primers(body: TaskBody) -> Dict[str, Any]:
return start_celery_task(body.data, CELERY, "tasks.pas", "export_pas")


@app.get("/v1/get_species")
def export_species_table_get() -> Any:
task_name = "tasks.species_table"
try:
task = CELERY.send_task(task_name, args=[{}])
final_res = task.wait(timeout=None, propagate=True, interval=0.5)
return final_res
except Exception as e:
raise HTTPException(status_code=400, detail=traceback.format_exc())


@app.post("/v1/species_table")
def export_species_table_post(body: TaskBody) -> Dict[str, Any]:
return start_celery_task(
body.data, CELERY, "tasks.species_table", "export_species_table"
)


def start_celery_task(
body: Any, celery_app: Celery, task_name: str, export_name: str
) -> Dict[str, Any]:
try:
task = celery_app.send_task(task_name, args=[body])
return get_urls(task.id, export_name)
except Exception as e:
raise HTTPException(status_code=400, detail=traceback.format_exc())


@app.get("/v1/check/{task_id}")
def check_task(task_id: str) -> str:
return CELERY.AsyncResult(task_id).state


@app.get("/v1/cancel/{task_id}")
def cancel_task(task_id: str) -> None:
CELERY.AsyncResult(task_id).revoke(terminate=True)


@app.get("/v1/forget/{task_id}")
def forget_task(task_id: str) -> None:
CELERY.AsyncResult(task_id).forget()


@app.get("/v1/result/{task_id}")
def check_task_result(task_id: str) -> Any:
async_result = CELERY.AsyncResult(task_id)
if async_result.successful():
return async_result.result
if async_result.failed():
return async_result.traceback


@app.get("/v1/export_qclm/{task_id}.xlsx")
def export_qclm(task_id: str) -> StreamingResponse:
task = CELERY.send_task("tasks.export_qclm", args=[task_id])
result = task.get()
if result is not None:
file_like = io.BytesIO(result.encode())
return StreamingResponse(
file_like,
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
headers={"Content-Disposition": f"attachment; filename={task_id}.xlsx"},
)
raise HTTPException(status_code=404, detail="Result not found")


@app.get("/v1/export_ssm/{task_id}.xlsx")
def export_ssm(task_id: str) -> StreamingResponse:
task = CELERY.send_task("tasks.export_ssm", args=[task_id])
result = task.get()
if result is not None:
file_like = io.BytesIO(binascii.a2b_base64(result))
return StreamingResponse(
file_like,
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
headers={"Content-Disposition": f"attachment; filename={task_id}.xlsx"},
)
raise HTTPException(status_code=404, detail="Result not found")


@app.get("/v1/export_species_table/{task_id}.xlsx")
def export_species_table(task_id: str) -> StreamingResponse:
task = CELERY.send_task("tasks.export_species_table", args=[task_id])
result = task.get()
if result is not None:
file_like = io.BytesIO(binascii.a2b_base64(result))
return StreamingResponse(
file_like,
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
headers={"Content-Disposition": f"attachment; filename={task_id}.xlsx"},
)
raise HTTPException(status_code=404, detail="Result not found")


if __name__ == "__main__":
import uvicorn

print("Mutation Maker version: 1.0.0")
uvicorn.run(app, host="0.0.0.0", port=8000)
4 changes: 2 additions & 2 deletions backend/requirements.txt
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
behave==1.2.6
biopython==1.85
boto3==1.36.25
biopython==1.86
boto3>=1.42.0
celery==5.4.0
flower==2.0.1
jmespath==1.0.1
Expand Down
Loading
Loading