From 207cffd56913ce14a67dd1dbcebc9ad12f557688 Mon Sep 17 00:00:00 2001 From: Alex Lubbock Date: Tue, 15 Oct 2024 15:16:53 +0100 Subject: [PATCH] feat: blast search dataset vs database (#99) * style: fix deprecated dockerfile syntax * feat: add BLAST to backend container * wip: all-vs-all BLAST * wip: blastp * fix: empty state for blast results * fix: ci build of test env * fix: bytes decoding in exception message * ci: ignore flake8 false positives * fix: restore types-pytz in dev env --- .github/workflows/ci.yml | 2 + backend/Dockerfile | 41 ++- backend/Pipfile.lock | 328 +++++++++--------- backend/antigenapi/bioinformatics/__init__.py | 0 backend/antigenapi/bioinformatics/blast.py | 137 ++++++++ .../imgt.py} | 0 backend/antigenapi/models.py | 2 +- backend/antigenapi/tests/test_parsers.py | 2 +- backend/antigenapi/views_old.py | 87 ++++- backend/setup.cfg | 10 +- docker-compose.yml | 4 + frontend/Dockerfile | 2 +- frontend/src/App.js | 6 + frontend/src/LoadingLlama.js | 54 +++ frontend/src/crudtemplates/BlastResults.js | 152 ++++++++ .../src/crudtemplates/SequencingResults.js | 10 +- 16 files changed, 642 insertions(+), 195 deletions(-) create mode 100644 backend/antigenapi/bioinformatics/__init__.py create mode 100644 backend/antigenapi/bioinformatics/blast.py rename backend/antigenapi/{bioinformatics.py => bioinformatics/imgt.py} (100%) create mode 100644 frontend/src/LoadingLlama.js create mode 100644 frontend/src/crudtemplates/BlastResults.js diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d9e27bc3..380799e5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -37,6 +37,8 @@ jobs: with: context: ${{ matrix.component }} target: dev + build-args: | + PIPENV_SYNC_FLAGS=--dev platforms: linux/amd64 load: true tags: antigen-app-dev-${{ matrix.component }}:latest diff --git a/backend/Dockerfile b/backend/Dockerfile index 1d7d7098..16035c59 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -1,14 +1,21 @@ -FROM python:3.11-bullseye AS builder +FROM quay.io/rosalindfranklininstitute/blast:2.16.0 AS blast -RUN pip install --user wheel pipenv +FROM python:3.13-bookworm AS builder + +RUN pip install --upgrade pip wheel pipenv + +RUN adduser --uid 10191 --group --system --no-create-home nonroot +RUN chown -R nonroot:nonroot /usr/src +USER nonroot +WORKDIR /usr/src # Tell pipenv to create venv in the current directory ENV PIPENV_VENV_IN_PROJECT=1 COPY Pipfile Pipfile.lock pyproject.toml setup.cfg /usr/src/ -WORKDIR /usr/src -RUN /root/.local/bin/pipenv sync +ARG PIPENV_SYNC_FLAGS= +RUN pipenv sync ${PIPENV_SYNC_FLAGS} COPY manage.py /usr/src/ COPY antigendjango /usr/src/antigendjango @@ -16,14 +23,23 @@ COPY antigenapi /usr/src/antigenapi RUN DJANGO_CI=true .venv/bin/python manage.py collectstatic --noinput -FROM python:3.11-slim-bullseye AS prod +FROM python:3.13-slim-bookworm AS prod +# liblmdb-dev required by BLAST RUN apt-get update && apt-get install -y \ libxml2 \ media-types \ + liblmdb-dev \ && rm -rf /var/lib/apt/lists/* -RUN mkdir -v /usr/src/.venv +RUN adduser --uid 10191 --group --system --no-create-home nonroot +RUN chown -R nonroot:nonroot /usr/src +USER nonroot +WORKDIR /usr/src + +# blastp and makeblastdb commands +COPY --from=blast /blast/ReleaseMT/bin/blastp /usr/local/bin/blastp +COPY --from=blast /blast/ReleaseMT/bin/makeblastdb /usr/local/bin/makeblastdb COPY --from=builder /usr/src/.venv/ /usr/src/.venv/ COPY --from=builder /usr/src/static/ /api_data/static/ @@ -34,16 +50,17 @@ COPY uwsgi.ini /usr/src WORKDIR /usr/src -RUN addgroup --gid 10191 nonroot -RUN adduser --uid 10191 --gid 10191 --system --no-create-home nonroot -USER nonroot - CMD [".venv/bin/uwsgi", "--ini", "uwsgi.ini"] FROM builder AS dev -ENV PATH="$PATH:/root/.local/bin:/usr/src/.venv/bin" +# liblmdb-dev required by BLAST +USER root +RUN apt update && apt install -y liblmdb-dev && rm -rf /var/lib/apt/lists/* +USER nonroot -RUN pipenv sync --dev +# blastp and makeblastdb commands +COPY --from=blast /blast/ReleaseMT/bin/blastp /usr/local/bin/blastp +COPY --from=blast /blast/ReleaseMT/bin/makeblastdb /usr/local/bin/makeblastdb CMD ["pipenv", "run", "python", "manage.py", "runserver", "0.0.0.0:8080"] diff --git a/backend/Pipfile.lock b/backend/Pipfile.lock index 839d1ede..26bc531f 100644 --- a/backend/Pipfile.lock +++ b/backend/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "418cdcd38a65d5257a15593a3da0757d179fa7a933e775d6b35a23b6197640c6" + "sha256": "125465e4974ab903b494eb09a16831177ac48ac052f095ca2555a027cb6433c0" }, "pipfile-spec": 6, "requires": {}, @@ -74,18 +74,19 @@ }, "boto3": { "hashes": [ - "sha256:385ca77bf8ea4ab2d97f6e2435bdb29f77d9301e2f7ac796c2f465753c2adf3c", - "sha256:470d981583885859fed2fd1c185eeb01cc03e60272d499bafe41b12625b158c8" + "sha256:33c6a7aeab316f7e0b3ad8552afe95a4a10bfd58519d00741c4d4f3047da8382", + "sha256:9352f6d61f15c789231a5d608613f03425059072ed862c32e1ed102b17206abf" ], - "version": "==1.35.37" + "markers": "python_version >= '3.8'", + "version": "==1.35.40" }, "botocore": { "hashes": [ - "sha256:64f965d4ba7adb8d79ce044c3aef7356e05dd74753cf7e9115b80f477845d920", - "sha256:b2b4d29bafd95b698344f2f0577bb67064adbf1735d8a0e3c7473daa59c23ba6" + "sha256:072cc47f29cb1de4fa77ce6632e4f0480af29b70816973ff415fbaa3f50bd1db", + "sha256:547e0a983856c7d7aeaa30fca2a283873c57c07366cd806d2d639856341b3c31" ], "markers": "python_version >= '3.8'", - "version": "==1.35.37" + "version": "==1.35.40" }, "bs4": { "hashes": [ @@ -248,11 +249,11 @@ }, "django-cors-headers": { "hashes": [ - "sha256:5c6e3b7fe870876a1efdfeb4f433782c3524078fa0dc9e0195f6706ce7a242f6", - "sha256:92cf4633e22af67a230a1456cb1b7a02bb213d6536d2dcb2a4a24092ea9cebc2" + "sha256:28c1ded847aa70208798de3e42422a782f427b8b720e8d7319d34b654b5978e6", + "sha256:6c01a85cf1ec779a7bde621db853aa3ce5c065a5ba8e27df7a9f9e8dac310f4f" ], - "markers": "python_version >= '3.8'", - "version": "==4.4.0" + "markers": "python_version >= '3.9'", + "version": "==4.5.0" }, "django-filter": { "hashes": [ @@ -837,15 +838,6 @@ "markers": "python_version >= '3.7'", "version": "==4.66.5" }, - "types-pytz": { - "hashes": [ - "sha256:3e22df1336c0c6ad1d29163c8fda82736909eb977281cb823c57f8bae07118b7", - "sha256:575dc38f385a922a212bac00a7d6d2e16e141132a3c955078f4a4fd13ed6cb44" - ], - "index": "pypi", - "markers": "python_version >= '3.8'", - "version": "==2024.2.0.20241003" - }, "typing-extensions": { "hashes": [ "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", @@ -1018,45 +1010,46 @@ }, "black": { "hashes": [ - "sha256:2818cf72dfd5d289e48f37ccfa08b460bf469e67fb7c4abb07edc2e9f16fb63f", - "sha256:41622020d7120e01d377f74249e677039d20e6344ff5851de8a10f11f513bf93", - "sha256:4acf672def7eb1725f41f38bf6bf425c8237248bb0804faa3965c036f7672d11", - "sha256:4be5bb28e090456adfc1255e03967fb67ca846a03be7aadf6249096100ee32d0", - "sha256:4f1373a7808a8f135b774039f61d59e4be7eb56b2513d3d2f02a8b9365b8a8a9", - "sha256:56f52cfbd3dabe2798d76dbdd299faa046a901041faf2cf33288bc4e6dae57b5", - "sha256:65b76c275e4c1c5ce6e9870911384bff5ca31ab63d19c76811cb1fb162678213", - "sha256:65c02e4ea2ae09d16314d30912a58ada9a5c4fdfedf9512d23326128ac08ac3d", - "sha256:6905238a754ceb7788a73f02b45637d820b2f5478b20fec82ea865e4f5d4d9f7", - "sha256:79dcf34b33e38ed1b17434693763301d7ccbd1c5860674a8f871bd15139e7837", - "sha256:7bb041dca0d784697af4646d3b62ba4a6b028276ae878e53f6b4f74ddd6db99f", - "sha256:7d5e026f8da0322b5662fa7a8e752b3fa2dac1c1cbc213c3d7ff9bdd0ab12395", - "sha256:9f50ea1132e2189d8dff0115ab75b65590a3e97de1e143795adb4ce317934995", - "sha256:a0c9c4a0771afc6919578cec71ce82a3e31e054904e7197deacbc9382671c41f", - "sha256:aadf7a02d947936ee418777e0247ea114f78aff0d0959461057cae8a04f20597", - "sha256:b5991d523eee14756f3c8d5df5231550ae8993e2286b8014e2fdea7156ed0959", - "sha256:bf21b7b230718a5f08bd32d5e4f1db7fc8788345c8aea1d155fc17852b3410f5", - "sha256:c45f8dff244b3c431b36e3224b6be4a127c6aca780853574c00faf99258041eb", - "sha256:c7ed6668cbbfcd231fa0dc1b137d3e40c04c7f786e626b405c62bcd5db5857e4", - "sha256:d7de8d330763c66663661a1ffd432274a2f92f07feeddd89ffd085b5744f85e7", - "sha256:e19cb1c6365fd6dc38a6eae2dcb691d7d83935c10215aef8e6c38edee3f77abd", - "sha256:e2af80566f43c85f5797365077fb64a393861a3730bd110971ab7a0c94e873e7" - ], - "version": "==24.3.0" + "sha256:14b3502784f09ce2443830e3133dacf2c0110d45191ed470ecb04d0f5f6fcb0f", + "sha256:17374989640fbca88b6a448129cd1745c5eb8d9547b464f281b251dd00155ccd", + "sha256:1c536fcf674217e87b8cc3657b81809d3c085d7bf3ef262ead700da345bfa6ea", + "sha256:1cbacacb19e922a1d75ef2b6ccaefcd6e93a2c05ede32f06a21386a04cedb981", + "sha256:1f93102e0c5bb3907451063e08b9876dbeac810e7da5a8bfb7aeb5a9ef89066b", + "sha256:2cd9c95431d94adc56600710f8813ee27eea544dd118d45896bb734e9d7a0dc7", + "sha256:30d2c30dc5139211dda799758559d1b049f7f14c580c409d6ad925b74a4208a8", + "sha256:394d4ddc64782e51153eadcaaca95144ac4c35e27ef9b0a42e121ae7e57a9175", + "sha256:3bb2b7a1f7b685f85b11fed1ef10f8a9148bceb49853e47a294a3dd963c1dd7d", + "sha256:4007b1393d902b48b36958a216c20c4482f601569d19ed1df294a496eb366392", + "sha256:5a2221696a8224e335c28816a9d331a6c2ae15a2ee34ec857dcf3e45dbfa99ad", + "sha256:63f626344343083322233f175aaf372d326de8436f5928c042639a4afbbf1d3f", + "sha256:649fff99a20bd06c6f727d2a27f401331dc0cc861fb69cde910fe95b01b5928f", + "sha256:680359d932801c76d2e9c9068d05c6b107f2584b2a5b88831c83962eb9984c1b", + "sha256:846ea64c97afe3bc677b761787993be4991810ecc7a4a937816dd6bddedc4875", + "sha256:b5e39e0fae001df40f95bd8cc36b9165c5e2ea88900167bddf258bacef9bbdc3", + "sha256:ccfa1d0cb6200857f1923b602f978386a3a2758a65b52e0950299ea014be6800", + "sha256:d37d422772111794b26757c5b55a3eade028aa3fde43121ab7b673d050949d65", + "sha256:ddacb691cdcdf77b96f549cf9591701d8db36b2f19519373d60d31746068dbf2", + "sha256:e6668650ea4b685440857138e5fe40cde4d652633b1bdffc62933d0db4ed9812", + "sha256:f9da3333530dbcecc1be13e69c250ed8dfa67f43c4005fb537bb426e19200d50", + "sha256:fe4d6476887de70546212c99ac9bd803d90b42fc4767f058a0baa895013fbb3e" + ], + "version": "==24.10.0" }, "boto3": { "hashes": [ - "sha256:385ca77bf8ea4ab2d97f6e2435bdb29f77d9301e2f7ac796c2f465753c2adf3c", - "sha256:470d981583885859fed2fd1c185eeb01cc03e60272d499bafe41b12625b158c8" + "sha256:33c6a7aeab316f7e0b3ad8552afe95a4a10bfd58519d00741c4d4f3047da8382", + "sha256:9352f6d61f15c789231a5d608613f03425059072ed862c32e1ed102b17206abf" ], - "version": "==1.35.37" + "markers": "python_version >= '3.8'", + "version": "==1.35.40" }, "botocore": { "hashes": [ - "sha256:64f965d4ba7adb8d79ce044c3aef7356e05dd74753cf7e9115b80f477845d920", - "sha256:b2b4d29bafd95b698344f2f0577bb67064adbf1735d8a0e3c7473daa59c23ba6" + "sha256:072cc47f29cb1de4fa77ce6632e4f0480af29b70816973ff415fbaa3f50bd1db", + "sha256:547e0a983856c7d7aeaa30fca2a283873c57c07366cd806d2d639856341b3c31" ], "markers": "python_version >= '3.8'", - "version": "==1.35.37" + "version": "==1.35.40" }, "certifi": { "hashes": [ @@ -1077,71 +1070,71 @@ "coverage": { "extras": ["toml"], "hashes": [ - "sha256:078a87519057dacb5d77e333f740708ec2a8f768655f1db07f8dfd28d7a005f0", - "sha256:087932079c065d7b8ebadd3a0160656c55954144af6439886c8bcf78bbbcde7f", - "sha256:0bbae11c138585c89fb4e991faefb174a80112e1a7557d507aaa07675c62e66b", - "sha256:0ff2ef83d6d0b527b5c9dad73819b24a2f76fdddcfd6c4e7a4d7e73ecb0656b4", - "sha256:12179eb0575b8900912711688e45474f04ab3934aaa7b624dea7b3c511ecc90f", - "sha256:1e5e92e3e84a8718d2de36cd8387459cba9a4508337b8c5f450ce42b87a9e760", - "sha256:2186369a654a15628e9c1c9921409a6b3eda833e4b91f3ca2a7d9f77abb4987c", - "sha256:21c0ea0d4db8a36b275cb6fb2437a3715697a4ba3cb7b918d3525cc75f726304", - "sha256:24500f4b0e03aab60ce575c85365beab64b44d4db837021e08339f61d1fbfe52", - "sha256:2b636a301e53964550e2f3094484fa5a96e699db318d65398cfba438c5c92171", - "sha256:343056c5e0737487a5291f5691f4dfeb25b3e3c8699b4d36b92bb0e586219d14", - "sha256:35a51598f29b2a19e26d0908bd196f771a9b1c5d9a07bf20be0adf28f1ad4f77", - "sha256:39d3b964abfe1519b9d313ab28abf1d02faea26cd14b27f5283849bf59479ff5", - "sha256:3ec528ae69f0a139690fad6deac8a7d33629fa61ccce693fdd07ddf7e9931fba", - "sha256:47ccb6e99a3031ffbbd6e7cc041e70770b4fe405370c66a54dbf26a500ded80b", - "sha256:4eea60c79d36a8f39475b1af887663bc3ae4f31289cd216f514ce18d5938df40", - "sha256:536f77f2bf5797983652d1d55f1a7272a29afcc89e3ae51caa99b2db4e89d658", - "sha256:5ed69befa9a9fc796fe015a7040c9398722d6b97df73a6b608e9e275fa0932b0", - "sha256:62ab4231c01e156ece1b3a187c87173f31cbeee83a5e1f6dff17f288dca93345", - "sha256:667952739daafe9616db19fbedbdb87917eee253ac4f31d70c7587f7ab531b4e", - "sha256:69f251804e052fc46d29d0e7348cdc5fcbfc4861dc4a1ebedef7e78d241ad39e", - "sha256:6c2ba1e0c24d8fae8f2cf0aeb2fc0a2a7f69b6d20bd8d3749fd6b36ecef5edf0", - "sha256:6e85830eed5b5263ffa0c62428e43cb844296f3b4461f09e4bdb0d44ec190bc2", - "sha256:7571e8bbecc6ac066256f9de40365ff833553e2e0c0c004f4482facb131820ef", - "sha256:7781f4f70c9b0b39e1b129b10c7d43a4e0c91f90c60435e6da8288efc2b73438", - "sha256:7926d8d034e06b479797c199747dd774d5e86179f2ce44294423327a88d66ca7", - "sha256:7b80fbb0da3aebde102a37ef0138aeedff45997e22f8962e5f16ae1742852676", - "sha256:7fca4a92c8a7a73dee6946471bce6d1443d94155694b893b79e19ca2a540d86e", - "sha256:84c4315577f7cd511d6250ffd0f695c825efe729f4205c0340f7004eda51191f", - "sha256:8d9c5d13927d77af4fbe453953810db766f75401e764727e73a6ee4f82527b3e", - "sha256:9681516288e3dcf0aa7c26231178cc0be6cac9705cac06709f2353c5b406cfea", - "sha256:97df87e1a20deb75ac7d920c812e9326096aa00a9a4b6d07679b4f1f14b06c90", - "sha256:9bcd51eeca35a80e76dc5794a9dd7cb04b97f0e8af620d54711793bfc1fbba4b", - "sha256:9c6b0c1cafd96213a0327cf680acb39f70e452caf8e9a25aeb05316db9c07f89", - "sha256:a5f81e68aa62bc0cfca04f7b19eaa8f9c826b53fc82ab9e2121976dc74f131f3", - "sha256:a663b180b6669c400b4630a24cc776f23a992d38ce7ae72ede2a397ce6b0f170", - "sha256:a7b2e437fbd8fae5bc7716b9c7ff97aecc95f0b4d56e4ca08b3c8d8adcaadb84", - "sha256:a867d26f06bcd047ef716175b2696b315cb7571ccb951006d61ca80bbc356e9e", - "sha256:aa68a6cdbe1bc6793a9dbfc38302c11599bbe1837392ae9b1d238b9ef3dafcf1", - "sha256:ab31fdd643f162c467cfe6a86e9cb5f1965b632e5e65c072d90854ff486d02cf", - "sha256:ad4ef1c56b47b6b9024b939d503ab487231df1f722065a48f4fc61832130b90e", - "sha256:b92f9ca04b3e719d69b02dc4a69debb795af84cb7afd09c5eb5d54b4a1ae2191", - "sha256:bb21bac7783c1bf6f4bbe68b1e0ff0d20e7e7732cfb7995bc8d96e23aa90fc7b", - "sha256:bf4eeecc9e10f5403ec06138978235af79c9a79af494eb6b1d60a50b49ed2869", - "sha256:bfde025e2793a22efe8c21f807d276bd1d6a4bcc5ba6f19dbdfc4e7a12160909", - "sha256:c37faddc8acd826cfc5e2392531aba734b229741d3daec7f4c777a8f0d4993e5", - "sha256:c71965d1ced48bf97aab79fad56df82c566b4c498ffc09c2094605727c4b7e36", - "sha256:c9192925acc33e146864b8cf037e2ed32a91fdf7644ae875f5d46cd2ef086a5f", - "sha256:c9df1950fb92d49970cce38100d7e7293c84ed3606eaa16ea0b6bc27175bb667", - "sha256:cdfcf2e914e2ba653101157458afd0ad92a16731eeba9a611b5cbb3e7124e74b", - "sha256:d03a060ac1a08e10589c27d509bbdb35b65f2d7f3f8d81cf2fa199877c7bc58a", - "sha256:d20c3d1f31f14d6962a4e2f549c21d31e670b90f777ef4171be540fb7fb70f02", - "sha256:e4ee15b267d2dad3e8759ca441ad450c334f3733304c55210c2a44516e8d5530", - "sha256:e8ea055b3ea046c0f66217af65bc193bbbeca1c8661dc5fd42698db5795d2627", - "sha256:ebabdf1c76593a09ee18c1a06cd3022919861365219ea3aca0247ededf6facd6", - "sha256:ebc94fadbd4a3f4215993326a6a00e47d79889391f5659bf310f55fe5d9f581c", - "sha256:ed5ac02126f74d190fa2cc14a9eb2a5d9837d5863920fa472b02eb1595cdc925", - "sha256:f01e53575f27097d75d42de33b1b289c74b16891ce576d767ad8c48d17aeb5e0", - "sha256:f361296ca7054f0936b02525646b2731b32c8074ba6defab524b79b2b7eeac72", - "sha256:f9035695dadfb397bee9eeaf1dc7fbeda483bf7664a7397a629846800ce6e276", - "sha256:fcad7d5d2bbfeae1026b395036a8aa5abf67e8038ae7e6a25c7d0f88b10a8e6a", - "sha256:ff797320dcbff57caa6b2301c3913784a010e13b1f6cf4ab3f563f3c5e7919db" + "sha256:04f2189716e85ec9192df307f7c255f90e78b6e9863a03223c3b998d24a3c6c6", + "sha256:0c6c0f4d53ef603397fc894a895b960ecd7d44c727df42a8d500031716d4e8d2", + "sha256:0ca37993206402c6c35dc717f90d4c8f53568a8b80f0bf1a1b2b334f4d488fba", + "sha256:12f9515d875859faedb4144fd38694a761cd2a61ef9603bf887b13956d0bbfbb", + "sha256:1990b1f4e2c402beb317840030bb9f1b6a363f86e14e21b4212e618acdfce7f6", + "sha256:2341a78ae3a5ed454d524206a3fcb3cec408c2a0c7c2752cd78b606a2ff15af4", + "sha256:23bb63ae3f4c645d2d82fa22697364b0046fbafb6261b258a58587441c5f7bd0", + "sha256:27bd5f18d8f2879e45724b0ce74f61811639a846ff0e5c0395b7818fae87aec6", + "sha256:2dc7d6b380ca76f5e817ac9eef0c3686e7834c8346bef30b041a4ad286449990", + "sha256:331b200ad03dbaa44151d74daeb7da2cf382db424ab923574f6ecca7d3b30de3", + "sha256:365defc257c687ce3e7d275f39738dcd230777424117a6c76043459db131dd43", + "sha256:37be7b5ea3ff5b7c4a9db16074dc94523b5f10dd1f3b362a827af66a55198175", + "sha256:3c2e6fa98032fec8282f6b27e3f3986c6e05702828380618776ad794e938f53a", + "sha256:40e8b1983080439d4802d80b951f4a93d991ef3261f69e81095a66f86cf3c3c6", + "sha256:43517e1f6b19f610a93d8227e47790722c8bf7422e46b365e0469fc3d3563d97", + "sha256:43b32a06c47539fe275106b376658638b418c7cfdfff0e0259fbf877e845f14b", + "sha256:43d6a66e33b1455b98fc7312b124296dad97a2e191c80320587234a77b1b736e", + "sha256:4c59d6a4a4633fad297f943c03d0d2569867bd5372eb5684befdff8df8522e39", + "sha256:52ac29cc72ee7e25ace7807249638f94c9b6a862c56b1df015d2b2e388e51dbd", + "sha256:54356a76b67cf8a3085818026bb556545ebb8353951923b88292556dfa9f812d", + "sha256:583049c63106c0555e3ae3931edab5669668bbef84c15861421b94e121878d3f", + "sha256:6d99198203f0b9cb0b5d1c0393859555bc26b548223a769baf7e321a627ed4fc", + "sha256:6da42bbcec130b188169107ecb6ee7bd7b4c849d24c9370a0c884cf728d8e976", + "sha256:6e484e479860e00da1f005cd19d1c5d4a813324e5951319ac3f3eefb497cc549", + "sha256:70a6756ce66cd6fe8486c775b30889f0dc4cb20c157aa8c35b45fd7868255c5c", + "sha256:70d24936ca6c15a3bbc91ee9c7fc661132c6f4c9d42a23b31b6686c05073bde5", + "sha256:71967c35828c9ff94e8c7d405469a1fb68257f686bca7c1ed85ed34e7c2529c4", + "sha256:79644f68a6ff23b251cae1c82b01a0b51bc40c8468ca9585c6c4b1aeee570e0b", + "sha256:87cd2e29067ea397a47e352efb13f976eb1b03e18c999270bb50589323294c6e", + "sha256:8d4c6ea0f498c7c79111033a290d060c517853a7bcb2f46516f591dab628ddd3", + "sha256:9134032f5aa445ae591c2ba6991d10136a1f533b1d2fa8f8c21126468c5025c6", + "sha256:921fbe13492caf6a69528f09d5d7c7d518c8d0e7b9f6701b7719715f29a71e6e", + "sha256:99670790f21a96665a35849990b1df447993880bb6463a0a1d757897f30da929", + "sha256:9975442f2e7a5cfcf87299c26b5a45266ab0696348420049b9b94b2ad3d40234", + "sha256:99ded130555c021d99729fabd4ddb91a6f4cc0707df4b1daf912c7850c373b13", + "sha256:a3328c3e64ea4ab12b85999eb0779e6139295bbf5485f69d42cf794309e3d007", + "sha256:a4fb91d5f72b7e06a14ff4ae5be625a81cd7e5f869d7a54578fc271d08d58ae3", + "sha256:aa23ce39661a3e90eea5f99ec59b763b7d655c2cada10729ed920a38bfc2b167", + "sha256:aac7501ae73d4a02f4b7ac8fcb9dc55342ca98ffb9ed9f2dfb8a25d53eda0e4d", + "sha256:ab84a8b698ad5a6c365b08061920138e7a7dd9a04b6feb09ba1bfae68346ce6d", + "sha256:b4adeb878a374126f1e5cf03b87f66279f479e01af0e9a654cf6d1509af46c40", + "sha256:b9853509b4bf57ba7b1f99b9d866c422c9c5248799ab20e652bbb8a184a38181", + "sha256:bb7d5fe92bd0dc235f63ebe9f8c6e0884f7360f88f3411bfed1350c872ef2054", + "sha256:bca4c8abc50d38f9773c1ec80d43f3768df2e8576807d1656016b9d3eeaa96fd", + "sha256:c222958f59b0ae091f4535851cbb24eb57fc0baea07ba675af718fb5302dddb2", + "sha256:c30e42ea11badb147f0d2e387115b15e2bd8205a5ad70d6ad79cf37f6ac08c91", + "sha256:c3a79f56dee9136084cf84a6c7c4341427ef36e05ae6415bf7d787c96ff5eaa3", + "sha256:c51ef82302386d686feea1c44dbeef744585da16fcf97deea2a8d6c1556f519b", + "sha256:c77326300b839c44c3e5a8fe26c15b7e87b2f32dfd2fc9fee1d13604347c9b38", + "sha256:d33a785ea8354c480515e781554d3be582a86297e41ccbea627a5c632647f2cd", + "sha256:d546cfa78844b8b9c1c0533de1851569a13f87449897bbc95d698d1d3cb2a30f", + "sha256:da29ceabe3025a1e5a5aeeb331c5b1af686daab4ff0fb4f83df18b1180ea83e2", + "sha256:df8c05a0f574d480947cba11b947dc41b1265d721c3777881da2fb8d3a1ddfba", + "sha256:e266af4da2c1a4cbc6135a570c64577fd3e6eb204607eaff99d8e9b710003c6f", + "sha256:e279f3db904e3b55f520f11f983cc8dc8a4ce9b65f11692d4718ed021ec58b83", + "sha256:ea52bd218d4ba260399a8ae4bb6b577d82adfc4518b93566ce1fddd4a49d1dce", + "sha256:ebec65f5068e7df2d49466aab9128510c4867e532e07cb6960075b27658dca38", + "sha256:ec1e3b40b82236d100d259854840555469fad4db64f669ab817279eb95cd535c", + "sha256:ee77c7bef0724165e795b6b7bf9c4c22a9b8468a6bdb9c6b4281293c6b22a90f", + "sha256:f263b18692f8ed52c8de7f40a0751e79015983dbd77b16906e5b310a39d3ca21", + "sha256:f7b26757b22faf88fcf232f5f0e62f6e0fd9e22a8a5d0d5016888cdfe1f6c1c4", + "sha256:f7ddb920106bbbbcaf2a274d56f46956bf56ecbde210d88061824a95bdd94e92" ], "markers": "python_version >= '3.9'", - "version": "==7.6.2" + "version": "==7.6.3" }, "django": { "hashes": [ @@ -1170,11 +1163,11 @@ }, "django-cors-headers": { "hashes": [ - "sha256:5c6e3b7fe870876a1efdfeb4f433782c3524078fa0dc9e0195f6706ce7a242f6", - "sha256:92cf4633e22af67a230a1456cb1b7a02bb213d6536d2dcb2a4a24092ea9cebc2" + "sha256:28c1ded847aa70208798de3e42422a782f427b8b720e8d7319d34b654b5978e6", + "sha256:6c01a85cf1ec779a7bde621db853aa3ce5c065a5ba8e27df7a9f9e8dac310f4f" ], - "markers": "python_version >= '3.8'", - "version": "==4.4.0" + "markers": "python_version >= '3.9'", + "version": "==4.5.0" }, "django-filter": { "hashes": [ @@ -1235,10 +1228,10 @@ }, "flake8": { "hashes": [ - "sha256:07528381786f2a6237b061f6e96610a4167b226cb926e2aa2b6b1d78057c576b", - "sha256:bf8fd333346d844f616e8d47905ef3a3384edae6b4e9beb0c5101e25e3110907" + "sha256:6fbe320aad8d6b95cec8b8e47bc933004678dc63095be98528b7bdd2a9f510db", + "sha256:7a1cf6b73744f5806ab95e526f6f0d8c01c66d7bbe349562d22dfca20610b248" ], - "version": "==3.9.2" + "version": "==5.0.4" }, "flake8-isort": { "hashes": [ @@ -1272,44 +1265,49 @@ }, "mccabe": { "hashes": [ - "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", - "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" + "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", + "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e" ], - "version": "==0.6.1" + "markers": "python_version >= '3.6'", + "version": "==0.7.0" }, "mypy": { "hashes": [ - "sha256:06d26c277962f3fb50e13044674aa10553981ae514288cb7d0a738f495550b36", - "sha256:2ff93107f01968ed834f4256bc1fc4475e2fecf6c661260066a985b52741ddce", - "sha256:36383a4fcbad95f2657642a07ba22ff797de26277158f1cc7bd234821468b1b6", - "sha256:37c7fa6121c1cdfcaac97ce3d3b5588e847aa79b580c1e922bb5d5d2902df19b", - "sha256:3a66169b92452f72117e2da3a576087025449018afc2d8e9bfe5ffab865709ca", - "sha256:3f14cd3d386ac4d05c5a39a51b84387403dadbd936e17cb35882134d4f8f0d24", - "sha256:41ea707d036a5307ac674ea172875f40c9d55c5394f888b168033177fce47383", - "sha256:478db5f5036817fe45adb7332d927daa62417159d49783041338921dcf646fc7", - "sha256:4a8a53bc3ffbd161b5b2a4fff2f0f1e23a33b0168f1c0778ec70e1a3d66deb86", - "sha256:539c570477a96a4e6fb718b8d5c3e0c0eba1f485df13f86d2970c91f0673148d", - "sha256:57555a7715c0a34421013144a33d280e73c08df70f3a18a552938587ce9274f4", - "sha256:6e658bd2d20565ea86da7d91331b0eed6d2eee22dc031579e6297f3e12c758c8", - "sha256:6e7184632d89d677973a14d00ae4d03214c8bc301ceefcdaf5c474866814c987", - "sha256:75746e06d5fa1e91bfd5432448d00d34593b52e7e91a187d981d08d1f33d4385", - "sha256:7f9993ad3e0ffdc95c2a14b66dee63729f021968bff8ad911867579c65d13a79", - "sha256:801780c56d1cdb896eacd5619a83e427ce436d86a3bdf9112527f24a66618fef", - "sha256:801ca29f43d5acce85f8e999b1e431fb479cb02d0e11deb7d2abb56bdaf24fd6", - "sha256:969ea3ef09617aff826885a22ece0ddef69d95852cdad2f60c8bb06bf1f71f70", - "sha256:a976775ab2256aadc6add633d44f100a2517d2388906ec4f13231fafbb0eccca", - "sha256:af8d155170fcf87a2afb55b35dc1a0ac21df4431e7d96717621962e4b9192e70", - "sha256:b499bc07dbdcd3de92b0a8b29fdf592c111276f6a12fe29c30f6c417dd546d12", - "sha256:cd953f221ac1379050a8a646585a29574488974f79d8082cedef62744f0a0104", - "sha256:d42a6dd818ffce7be66cce644f1dff482f1d97c53ca70908dff0b9ddc120b77a", - "sha256:e8960dbbbf36906c5c0b7f4fbf2f0c7ffb20f4898e6a879fcf56a41a08b0d318", - "sha256:edb91dded4df17eae4537668b23f0ff6baf3707683734b6a818d5b9d0c0c31a1", - "sha256:ee23de8530d99b6db0573c4ef4bd8f39a2a6f9b60655bf7a1357e585a3486f2b", - "sha256:f7821776e5c4286b6a13138cc935e2e9b6fde05e081bdebf5cdb2bb97c9df81d" - ], - "index": "pypi", - "markers": "python_version >= '3.8'", - "version": "==1.11.2" + "sha256:060a07b10e999ac9e7fa249ce2bdcfa9183ca2b70756f3bce9df7a92f78a3c0a", + "sha256:06de0498798527451ffb60f68db0d368bd2bae2bbfb5237eae616d4330cc87aa", + "sha256:0eff042d7257f39ba4ca06641d110ca7d2ad98c9c1fb52200fe6b1c865d360ff", + "sha256:1ebf9e796521f99d61864ed89d1fb2926d9ab6a5fab421e457cd9c7e4dd65aa9", + "sha256:20c7c5ce0c1be0b0aea628374e6cf68b420bcc772d85c3c974f675b88e3e6e57", + "sha256:233e11b3f73ee1f10efada2e6da0f555b2f3a5316e9d8a4a1224acc10e7181d3", + "sha256:2c40658d4fa1ab27cb53d9e2f1066345596af2f8fe4827defc398a09c7c9519b", + "sha256:2f106db5ccb60681b622ac768455743ee0e6a857724d648c9629a9bd2ac3f721", + "sha256:4397081e620dc4dc18e2f124d5e1d2c288194c2c08df6bdb1db31c38cd1fe1ed", + "sha256:48d3e37dd7d9403e38fa86c46191de72705166d40b8c9f91a3de77350daa0893", + "sha256:4ae8959c21abcf9d73aa6c74a313c45c0b5a188752bf37dace564e29f06e9c1b", + "sha256:4b86de37a0da945f6d48cf110d5206c5ed514b1ca2614d7ad652d4bf099c7de7", + "sha256:52b9e1492e47e1790360a43755fa04101a7ac72287b1a53ce817f35899ba0521", + "sha256:5bc81701d52cc8767005fdd2a08c19980de9ec61a25dbd2a937dfb1338a826f9", + "sha256:5feee5c74eb9749e91b77f60b30771563327329e29218d95bedbe1257e2fe4b0", + "sha256:65a22d87e757ccd95cbbf6f7e181e6caa87128255eb2b6be901bb71b26d8a99d", + "sha256:684a9c508a283f324804fea3f0effeb7858eb03f85c4402a967d187f64562469", + "sha256:6b5df6c8a8224f6b86746bda716bbe4dbe0ce89fd67b1fa4661e11bfe38e8ec8", + "sha256:6cabe4cda2fa5eca7ac94854c6c37039324baaa428ecbf4de4567279e9810f9e", + "sha256:77278e8c6ffe2abfba6db4125de55f1024de9a323be13d20e4f73b8ed3402bd1", + "sha256:8462655b6694feb1c99e433ea905d46c478041a8b8f0c33f1dab00ae881b2164", + "sha256:923ea66d282d8af9e0f9c21ffc6653643abb95b658c3a8a32dca1eff09c06475", + "sha256:9b9ce1ad8daeb049c0b55fdb753d7414260bad8952645367e70ac91aec90e07e", + "sha256:a64ee25f05fc2d3d8474985c58042b6759100a475f8237da1f4faf7fcd7e6309", + "sha256:bfe012b50e1491d439172c43ccb50db66d23fab714d500b57ed52526a1020bb7", + "sha256:c72861b7139a4f738344faa0e150834467521a3fba42dc98264e5aa9507dd601", + "sha256:dcfb754dea911039ac12434d1950d69a2f05acd4d56f7935ed402be09fad145e", + "sha256:dee78a8b9746c30c1e617ccb1307b351ded57f0de0d287ca6276378d770006c0", + "sha256:e478601cc3e3fa9d6734d255a59c7a2e5c2934da4378f3dd1e3411ea8a248642", + "sha256:eafc1b7319b40ddabdc3db8d7d48e76cfc65bbeeafaa525a4e0fa6b76175467f", + "sha256:faca7ab947c9f457a08dcb8d9a8664fd438080e002b0fa3e41b0535335edcf7f", + "sha256:fd313226af375d52e1e36c383f39bf3836e1f192801116b31b090dfcd3ec5266" + ], + "markers": "python_version >= '3.11'", + "version": "==1.12.0" }, "mypy-extensions": { "hashes": [ @@ -1546,11 +1544,11 @@ }, "pycodestyle": { "hashes": [ - "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068", - "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef" + "sha256:2c9607871d58c76354b697b42f5d57e1ada7d261c261efac224b664affdc5785", + "sha256:d1735fc58b418fd7c5f658d28d943854f8a849b01a5d0a1e6f3f3fdd0166804b" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==2.7.0" + "markers": "python_version >= '3.6'", + "version": "==2.9.1" }, "pydocstyle": { "hashes": [ @@ -1562,11 +1560,11 @@ }, "pyflakes": { "hashes": [ - "sha256:7893783d01b8a89811dd72d7dfd4d84ff098e5eed95cfa8905b22bbffe52efc3", - "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db" + "sha256:4579f67d887f804e67edb544428f264b7b24f435b263c4614f384135cea553d2", + "sha256:491feb020dca48ccc562a8c0cbe8df07ee13078df59813b83959cbdada312ea3" ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==2.3.1" + "markers": "python_version >= '3.6'", + "version": "==2.5.0" }, "pytest": { "hashes": [ @@ -1597,10 +1595,10 @@ }, "pytest-flake8": { "hashes": [ - "sha256:358d449ca06b80dbadcb43506cd3e38685d273b4968ac825da871bd4cc436202", - "sha256:f1b19dad0b9f0aa651d391c9527ebc20ac1a0f847aa78581094c747462bfa182" + "sha256:44a805bd7dce16a0de921a1292df639b70d56ca621a68d4bb515e09858e6e429", + "sha256:7b8dfa77be42b5cec901b6b67eafb18d47ae6a05c4deac89399dba4c3591ce4d" ], - "version": "==1.1.0" + "version": "==1.2.2" }, "pytest-mypy": { "hashes": [ diff --git a/backend/antigenapi/bioinformatics/__init__.py b/backend/antigenapi/bioinformatics/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/backend/antigenapi/bioinformatics/blast.py b/backend/antigenapi/bioinformatics/blast.py new file mode 100644 index 00000000..7e26de38 --- /dev/null +++ b/backend/antigenapi/bioinformatics/blast.py @@ -0,0 +1,137 @@ +import os +import subprocess +from tempfile import TemporaryDirectory +from typing import Optional + +from ..models import SequencingRunResults +from .imgt import read_airr_file + +# https://www.ncbi.nlm.nih.gov/books/NBK279684/table/appendices.T.options_common_to_all_blast/ +BLAST_FMT_MULTIPLE_FILE_BLAST_JSON = "15" +BLAST_NUM_THREADS = 4 + + +def get_db_fasta(include_run: Optional[int] = None, exclude_run: Optional[int] = None): + """Get the sequencing database in fasta format. + + Args: + include_run (int, optional): Sequencing run ID to include. + Defaults to None. + exclude_run (int, optional): Sequencing run ID to exclude. + Defaults to None. + + Returns: + str: Sequencing run as a FASTA format string + """ + fasta_data = "" + query = SequencingRunResults.objects.all() + if include_run: + query = query.filter(sequencing_run_id=include_run) + if exclude_run: + query = query.exclude(sequencing_run_id=exclude_run) + for sr in query: + airr_file = read_airr_file( + sr.airr_file, usecols=("sequence_id", "sequence_alignment_aa") + ) + airr_file = airr_file[airr_file.sequence_alignment_aa.notna()] + if not airr_file.empty: + for _, row in airr_file.iterrows(): + fasta_data += f"> {row.sequence_id}\n" + fasta_data += f"{row.sequence_alignment_aa.replace('.', '')}\n" + return fasta_data + + +def get_sequencing_run_fasta(sequencing_run_id: int): + """Get sequencing run in BLAST format. + + Args: + sequencing_run_id (int): Sequencing run ID + + Returns: + str: Sequencing run as a FASTA format string + """ + return get_db_fasta(include_run=sequencing_run_id) + + +def run_blastp( + sequencing_run_id: int, outfmt: str = BLAST_FMT_MULTIPLE_FILE_BLAST_JSON +): + """Run blastp for a sequencing run vs rest of database. + + Args: + sequencing_run_id (int): Sequencing run ID. + + Returns: + JSONResponse: Single file BLAST JSON + """ + db_data = get_db_fasta() + if not db_data: + return None + query_data = get_sequencing_run_fasta(sequencing_run_id) + if not query_data: + return None + + # Write the DB to disk as .fasta format + with TemporaryDirectory() as tmp_dir: + fasta_filename = os.path.join(tmp_dir, "db.fasta") + with open(fasta_filename, "w") as f: + f.write(db_data) + + # Run makeblastdb + mkdb_proc = subprocess.run( + [ + "makeblastdb", + "-in", + "db.fasta", + "-dbtype", + "prot", + "-out", + "antigen.db", + ], + capture_output=True, + cwd=tmp_dir, + ) + + if mkdb_proc.returncode != 0: + raise Exception( + f"makeblastdb returned exit code of " + f"{mkdb_proc.returncode}\n\n" + f"STDOUT: {mkdb_proc.stdout.decode()}\n\n" + f"STDERR: {mkdb_proc.stderr.decode()}" + ) + + # Write out query file + fasta_filename = os.path.join(tmp_dir, "query.fasta") + with open(fasta_filename, "w") as f: + f.write(query_data) + + # Run blastp + blastp_proc = subprocess.run( + [ + "blastp", + "-db", + "antigen.db", + "-query", + "query.fasta", + "-outfmt", + outfmt, + "-out", + "antigen.results", + "-num_threads", + str(BLAST_NUM_THREADS), + ], + capture_output=True, + cwd=tmp_dir, + ) + + if blastp_proc.returncode != 0: + raise Exception( + f"blastp returned exit code of " + f"{blastp_proc.returncode}\n\n" + f"STDOUT: {blastp_proc.stdout.decode()}\n\n" + f"STDERR: {blastp_proc.stderr.decode()}" + ) + + # Read in the results file + with open(os.path.join(tmp_dir, "antigen.results"), "r") as f: + return f.read() diff --git a/backend/antigenapi/bioinformatics.py b/backend/antigenapi/bioinformatics/imgt.py similarity index 100% rename from backend/antigenapi/bioinformatics.py rename to backend/antigenapi/bioinformatics/imgt.py diff --git a/backend/antigenapi/models.py b/backend/antigenapi/models.py index ed20cf6a..52871f03 100644 --- a/backend/antigenapi/models.py +++ b/backend/antigenapi/models.py @@ -28,7 +28,7 @@ ) from django.db.models.signals import post_init, post_save -from .bioinformatics import read_airr_file +from .bioinformatics.imgt import read_airr_file class Project(Model): diff --git a/backend/antigenapi/tests/test_parsers.py b/backend/antigenapi/tests/test_parsers.py index b8f13d25..563f72c7 100644 --- a/backend/antigenapi/tests/test_parsers.py +++ b/backend/antigenapi/tests/test_parsers.py @@ -2,7 +2,7 @@ import unittest import zipfile -from antigenapi.bioinformatics import load_sequences, trim_sequence +from antigenapi.bioinformatics.imgt import load_sequences, trim_sequence from antigenapi.views_old import _extract_well diff --git a/backend/antigenapi/views_old.py b/backend/antigenapi/views_old.py index ffbc5f21..d82ccce8 100644 --- a/backend/antigenapi/views_old.py +++ b/backend/antigenapi/views_old.py @@ -1,6 +1,7 @@ import collections.abc import datetime import io +import json import math import os import re @@ -36,7 +37,8 @@ from rest_framework.views import APIView from rest_framework.viewsets import ModelViewSet -from antigenapi.bioinformatics import ( +from antigenapi.bioinformatics.blast import get_db_fasta, run_blastp +from antigenapi.bioinformatics.imgt import ( AIRR_IMPORTANT_COLUMNS, as_fasta_files, load_sequences, @@ -1052,20 +1054,83 @@ def search_sequencing_run_results(self, request, query): {"matches": pd.concat(results).to_dict(orient="records") if results else []} ) + @action( + detail=True, + methods=["GET"], + name="BLAST sequencing run results vs DB.", + url_path="blast", + ) + def get_blast_sequencing_run(self, request, pk): + """BLAST sequencing run vs database.""" + blast_str = run_blastp(pk) + if not blast_str: + return JsonResponse({"hits": []}, status=status.HTTP_404_NOT_FOUND) + + # Read query AIRR files for CDRs + results = SequencingRunResults.objects.filter(sequencing_run_id=int(pk)) + + airr_df = pd.concat(read_airr_file(r.airr_file) for r in results) + airr_df = airr_df.set_index("sequence_id") + + ALIGN_PERC_THRESHOLD = 90 + E_VALUE_THRESHOLD = 0.05 + + # Parse the JSON and keep the important bits + blast_result = json.loads(blast_str) + res = [] + for blast_run in blast_result["BlastOutput2"]: + run_res = blast_run["report"]["results"]["search"] + for blast_hit_set in run_res["hits"]: + subject_title = blast_hit_set["description"][0]["title"] + query_title = run_res["query_title"] + query_cdr3 = airr_df.at[query_title, "cdr3_aa"] + if pd.isna(query_cdr3): + query_cdr3 = None + if subject_title.strip() == query_title.strip(): + continue + hsps = blast_hit_set["hsps"][0] + assert len(blast_hit_set["description"]) == 1 + for hsps in blast_hit_set["hsps"]: + align_perc = round( + (hsps["align_len"] / run_res["query_len"]) * 100, 2 + ) + if align_perc < ALIGN_PERC_THRESHOLD: + continue + e_value = hsps["evalue"] + if e_value > E_VALUE_THRESHOLD: + continue + res.append( + { + "query_title": query_title.strip(), + "query_cdr3": query_cdr3, + "subject_title": subject_title.strip(), + "submatch_no": hsps["num"], + "query_seq": hsps["qseq"], + "subject_seq": hsps["hseq"], + "midline": hsps["midline"], + "bit_score": hsps["bit_score"], + "e_value": e_value, + "align_len": hsps["align_len"], + "align_perc": align_perc, + "ident_perc": round( + (hsps["identity"] / hsps["align_len"]) * 100, 2 + ), + } + ) + + # Sort the results + res = sorted( + res, + key=lambda r: (r["query_cdr3"] or "", -r["align_perc"], -r["ident_perc"]), + ) + + return JsonResponse({"hits": res}) + class GlobalFastaView(APIView): def get(self, request, format=None): """Download entire database as .fasta file.""" - fasta_data = "" - for sr in SequencingRunResults.objects.all(): - airr_file = read_airr_file( - sr.airr_file, usecols=("sequence_id", "sequence_alignment_aa") - ) - airr_file = airr_file[airr_file.sequence_alignment_aa.notna()] - if not airr_file.empty: - for _, row in airr_file.iterrows(): - fasta_data += f"> {row.sequence_id}\n" - fasta_data += f"{row.sequence_alignment_aa.replace('.', '')}\n" + fasta_data = get_db_fasta() fasta_filename = ( f"antigenapp_database_{datetime.datetime.now().isoformat()}.fasta" diff --git a/backend/setup.cfg b/backend/setup.cfg index c86716da..fd7776a3 100644 --- a/backend/setup.cfg +++ b/backend/setup.cfg @@ -26,9 +26,9 @@ install_requires = [options.extras_require] dev = - black==24.3.0 - isort>5.0 - flake8<4 + black + isort + flake8 pytest pytest-cov pytest-black @@ -37,6 +37,7 @@ dev = pytest-flake8 pytest-black pytest-pydocstyle + types-pytz flake8-isort @@ -57,6 +58,9 @@ float_to_top=true max-line-length = 88 extend-ignore = E203, + E231, # false positive + E702, # false positive + E713, # false positive F811, F722, exclude = diff --git a/docker-compose.yml b/docker-compose.yml index 86921927..2c1a6f26 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -16,8 +16,11 @@ services: build: context: backend target: dev + args: + PIPENV_SYNC_FLAGS: --dev restart: always volumes: + - api_venv:/usr/src/.venv - ./backend:/usr/src - api_data:/api_data environment: @@ -43,3 +46,4 @@ services: volumes: api_data: + api_venv: diff --git a/frontend/Dockerfile b/frontend/Dockerfile index e87a9b65..6d65a506 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -1,6 +1,6 @@ FROM registry.hub.docker.com/library/node:22-alpine AS dev -ENV ANTIGENAPP_DIR /antigenapp +ENV ANTIGENAPP_DIR=/antigenapp COPY package-lock.json ${ANTIGENAPP_DIR}/package-lock.json COPY package.json ${ANTIGENAPP_DIR}/package.json diff --git a/frontend/src/App.js b/frontend/src/App.js index 4130fdfb..514c9d53 100644 --- a/frontend/src/App.js +++ b/frontend/src/App.js @@ -23,6 +23,7 @@ import * as Sentry from "@sentry/react"; import { BrowserTracing } from "@sentry/tracing"; import { getCookie } from "./crudtemplates/utils.js"; import SequencingResults from "./crudtemplates/SequencingResults.js"; +import BlastResults from "./crudtemplates/BlastResults.js"; if (process.env.REACT_APP_SENTRY_DSN !== undefined) { Sentry.init({ @@ -737,6 +738,11 @@ const App = () => { schema={schemas.sequencing} onSetError={setError} > + { + const [isShowing] = useState(true); + + useEffect(() => { + // setIsShowing(props.loading); + }, []); + + return ( + +
+
+ + AntigenApp loading indicator + Loading... +
+
+
+ ); +}; + +export default LoadingLlama; diff --git a/frontend/src/crudtemplates/BlastResults.js b/frontend/src/crudtemplates/BlastResults.js new file mode 100644 index 00000000..51076d43 --- /dev/null +++ b/frontend/src/crudtemplates/BlastResults.js @@ -0,0 +1,152 @@ +import config from "../config.js"; +import { useState, useEffect } from "react"; +import { useParams } from "react-router-dom"; +import * as Sentry from "@sentry/browser"; +import LoadingLlama from "../LoadingLlama.js"; + +const BlastResults = (props) => { + const { recordId } = useParams(); + const [blastResults, setBlastResults] = useState(); + + function classNames(...classes) { + return classes.filter(Boolean).join(" "); + } + + const extraPad = 2; + + const formatAlignment = (row) => { + let padLength = + Math.max(row.query_title.length, row.subject_title.length) + extraPad; + return ( +
+        {row.query_title.padEnd(padLength, " ")}
+        {row.query_seq}
+        
+ {"".padEnd(padLength, " ")} + {row.midline} +
+ {row.subject_title.padEnd(padLength, " ")} + {row.subject_seq} +
+ ); + }; + + const fetchBlastResults = () => { + fetch(config.url.API_URL + "/sequencingrun/" + recordId + "/blast/", { + method: "GET", + headers: { + "Content-Type": "application/json", + "X-CSRFToken": props.csrfToken, + }, + }) + .then((res) => { + res.json().then( + (data) => { + setBlastResults(data); + }, + (err) => { + console.log(res, err); + props.onSetError("HTTP code " + res.status); + }, + ); + }) + .catch((err) => { + Sentry.captureException(err); + props.onSetError(err.toString()); + }); + }; + + useEffect(() => { + fetchBlastResults(); + }, []); // eslint-disable-line react-hooks/exhaustive-deps + + return ( + <> + {!blastResults && } + {blastResults && ( +
+
+
+ + + + + + + + + + + + {!blastResults.hits.length && ( + + + + )} + {blastResults.hits.map((row, rowIdx, arr) => ( + 0 && + arr[rowIdx - 1].query_cdr3 !== row.query_cdr3 + ? "border-t-2 border-black" + : "", + )} + > + + + + + + + ))} + +
+ Query CDR3 + + Align. length + + Alignment + + e-value + + Bit score +
+ No results +
+ {row.query_cdr3 || "None"} + + {row.align_len} ({row.align_perc}%) + + {formatAlignment(row)} + + {row.e_value} + + {row.bit_score} +
+
+
+
+ )} + + ); +}; + +export default BlastResults; diff --git a/frontend/src/crudtemplates/SequencingResults.js b/frontend/src/crudtemplates/SequencingResults.js index 169f95be..01dd1afa 100644 --- a/frontend/src/crudtemplates/SequencingResults.js +++ b/frontend/src/crudtemplates/SequencingResults.js @@ -2,6 +2,7 @@ import config from "../config.js"; import { useState, useEffect } from "react"; import { useParams } from "react-router-dom"; import * as Sentry from "@sentry/browser"; +import LoadingLlama from "../LoadingLlama.js"; const SequencingResults = (props) => { const { recordId } = useParams(); @@ -42,7 +43,7 @@ const SequencingResults = (props) => { return ( <> - {!sequencingResults && "..."} + {!sequencingResults && } {sequencingResults && (
@@ -119,6 +120,13 @@ const SequencingResults = (props) => { + {!sequencingResults.records.length && ( + + + No results + + + )} {sequencingResults.records.map((row) => (