diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..7a7cc371 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,79 @@ +# Change Log + +All notable changes to this project will be documented in this file. + +## [0.3] 2017-03-05 + +### Added +- Created Chiron: a new service for Querying DINTO built + with [Apache Jena](https://jena.apache.org/). DINTO is turned into a triple + store at compile time, so boot time is greatly improved. Similarly, query + times are much improved by the triple store +- DDI analysis is now presented to users. After validating a PML file, Panacea + queries Asclepius to find any DDIs between drugs in the file. This is then + displayed to the user in the UI. + +### Removed +- DINTO from Asclepius. Chiron houses DINTO data now. Asclepius now proxies + queries to Chiron. +- Removed code that waited for DINTO to be loaded. Previously, Asclepius would + take several minutes to load DINTO into memory so Panacea would have to poll + it to see if it was ready. Chiron can load DINTO from its triple store + instantly, so the waiting is no longer required. + +## [0.2] 2017-02-26 + +### Added +- docker-compose. As we now have more than one docker service docker-compose is + used to easily coordinate them. docker-compose can be installed as per the + installation instructions in the README +- Panacea: a new docker service that is responsible for validating PML and + serving the UI. The UI is a web app +- Parser for PML. Added a custom lexer and parser for PML. Panacea will expose a + HTTP API for uploading files to be parsed. +- [Continuous Integration testing](https://circleci.com/gh/tom-and-the-toothfairies/pathways). + Automated testing has been added. CI runs whenever a commit is pushed. Merging + into master and our iteration branches requires CI to pass. +- Asclepius: a new service for querying DINTO. This service loads DINTO into + memory when it boots and exposes a HTTP API for making queries. Panacea will + use this API to provide users with DDI feedback. +- PML upload. Added a form that allows users to upload a file. The file is sent to Panacea for analysis. +- PML error & warning highlights. The results of file analysis are now reported back to the user. +- Asclepius DDI endpoint. Asclepius now exposes an endpoint that takes a list of + drugs and returns any DDIs between those drugs. + +### Removed +- Removed Pathways. The old docker service that + contained [peos](https://github.com/jnoll/peos) has been removed. It is + replaced by Asclepius and Panacea. +- Removed submodules. Previously Pathways used submodules as part of its docker + build process. Now Asclepius clones DINTO and checks out a specific revision + instead. + + +## [0.1] 2017-02-12 + +### Fixed +- Fixed installation instructions. Added a workaround for a DNS issue on the TCD network. + +### Added +- Instructions on how to run docker without sudo on Ubuntu. + +## [0.0] 2017-02-05 + +### Added +- Peos submodule. For now we'll use Peos to interact with PML. This will + probably be replaced by a custom parser later as we really only need the + parsing. +- DINTO submodule. The project depends + on [DINTO](https://github.com/labda/DINTO) to provide information about + drug-drug interactions (DDIs) +- Dockerfile for building project. At the moment, the project consists of one + docker service: pathways. +- Installation instructions in README + +[Unreleased]: https://github.com/tom-and-the-toothfairies/pathways/compare/0.3...release-1 +[0.3]: https://github.com/tom-and-the-toothfairies/pathways/compare/0.2...0.3 +[0.2]: https://github.com/tom-and-the-toothfairies/pathways/compare/0.1...0.2 +[0.1]: https://github.com/tom-and-the-toothfairies/pathways/compare/0.0...0.1 +[0.0]: https://github.com/tom-and-the-toothfairies/pathways/compare/faf0500c792aebbee26541ea2c25ad6ae274b2d5...0.0 diff --git a/FEATURES.md b/FEATURES.md new file mode 100644 index 00000000..bf32f719 --- /dev/null +++ b/FEATURES.md @@ -0,0 +1,180 @@ +# Features + +Each deliverable feature for the project is outlined in this file. Each feature +is given a short description. For completed features, instructions on how to +verify the feature are provided. + +Continuous integration testing has been set up for the project and can be +tracked [here](https://circleci.com/gh/tom-and-the-toothfairies/pathways). + +To manually verify features, run the project as outlined in the [README]. +The homepage is available at [localhost:4000](http://localhost:4000). + +## PML File Selection - Complete + +### Description +The system allows users to upload PML files for analysis. Users must be able to +select these files from their local file system. + +### Testing +Currently, this feature can only be tested manually. Visit the [homepage]. Click +the `Choose File` button and select a file. The chosen file should appear +as selected. + +## PML File Loading - Complete + +### Description +Once a file has been selected, users must be able to load it into the system for analysis. + +### Testing +This feature has automated tests which can be run with the following command + +```bash +$ docker run -e "MIX_ENV=test" tomtoothfairies/panacea mix test --only pml_loading +``` + +This feature can also be tested manually. Visit the [homepage] and +select a file. Press the `Submit` button. The file should be sent to the system, +and analysis results should now be displayed. + + +## Running PML Analysis - Complete + +### Description +When a file is submitted, the system must analyse it. The system must ensure +that it is a valid PML file. Invalid files must be rejected, and information +about the encountered error must be readily available. + +### Testing +This feature has automated tests which can be run with the following command + +```bash +$ docker run -e "MIX_ENV=test" tomtoothfairies/panacea mix test --only pml_analysis +``` + +This feature can also be tested manually. Visit the [homepage] and select a +file. Press the `Submit` button. The file should be sent to the system, and +analysis results should now be displayed. Invalid files should result in an +error dialogue, which displays a meaningful error message. Valid files should +result in a success dialogue. + +## On-Screen PML Reporting - Complete + +### Description +The results of analysing a file must be made available to the user. Any errors +must be easily identified, and the error messages must be useful. + +### Testing +This feature has automated tests which can be run with the following command + +```bash +$ docker run -e "MIX_ENV=test" tomtoothfairies/panacea mix test --only err_highlights +``` + +This feature can also be tested manually. Visit the [homepage] and select a +file. Some useful files can be found in the [fixtures directory]. Press the +`Submit` button. Analysis results should be displayed. `example.png` is not +UTF-8 encoded and should result in an `invalid filetype` error. `bad.pml` +contains invalid PML and should result in a `syntax error`. `ddis.pml` contains +valid PML and should result in a successful analysis. + +## PML Log-file Generation - Complete + +### Description +The successful or insuccessful loading of PML files into the system is output +to the console logs of the panacea service. + +### Testing +This feature is tested manually. + +First, open up the tail of the panacea logs: +```bash +$ docker-compose logs -f panacea +``` +Visit the [homepage] and select a file. Some useful files can be found in the +[fixtures directory]. Press the `Submit` button. You should see log entries +created indicating either a successful parse of the PML file and the drugs +contained in it or that an error occurred parsing the PML file and what the +error was. + +## PML Error and Warning highlights - Complete + +See [On-Screen PML Reporting](#on-screen-pml-reporting---complete) + +## Select specific OWL Ontology + +## Load Selected Ontology + +## Identify drugs in PML - Complete + +### Description +The system must identify drug-drug interactions between any drugs in a PML +file. To do this the system must be able to identify drugs in a given PML file. +We have chosen to use CHEBI and DINTO identifiers to denote drugs in PML. These +identifiers take the form `chebi:\d+` or `dinto:DB\d+` where `\d+` is any +sequence of digits. + +Drugs to be administered to patients must be placed in `requires` blocks within +the PML document. For example + +``` +process foo { + task bar { + requires { "chebi:1234" } + } +} +``` + +When a PML file is successfully analysed, any drugs found in `requires` blocks +must be reported back to the user as `identified drugs`. + +### Testing +This feature has automated tests which can be run with the following command + +```bash +$ docker run -e "MIX_ENV=test" tomtoothfairies/panacea mix test --only identify_drugs +``` + +This feature can also be manually tested by uploading files. Some useful files +can be found in the [fixtures directory]. Visit the homepage, select a file, +then press `Submit`. + +`no_ddis.pml` is a well-structured PML document containing some drug identifiers +and analysing it should result in drugs being identified and presented in the UI. + +`no_drugs.pml` is a well-structured PML document that does not contain drug +identifiers. Analysing it should result in no drugs being identified. + +## Identify drugs in DINTO + +## Identify DDIs - Complete + +### Description +The system must identify any drug-drug interactions between drugs in the +uploaded PML files. The drug-drug interactions are contained in DINTO. + +### Testing +This feature has automated tests which can be run with the following command + +```bash +$ docker run -e "MIX_ENV=test" tomtoothfairies/panacea mix test --only identify_ddis +``` + +This feature can also be manually tested by uploading files. Some usefull files +can be found in the [fixtures directory]. Visit the homepage, select a file, then press `Submit`. + +`no_ddis.pml` contains drugs that do not interact. Analysing this file should +result in no DDIs being reported to the user. + +`ddis.pml` contains drugs that do interact. Analysing this file should +result in DDIs being reported to the user. + +## On-Screen DINTO Reporting + +## DINTO Logfile Generation + +## DINTO Error and Warning highlights + +[README]: ./README.org +[homepage]: http://localhost:4000 +[fixtures directory]: ./panacea/test/fixtures diff --git a/README.org b/README.org index d85ce593..63c9f658 100644 --- a/README.org +++ b/README.org @@ -2,7 +2,7 @@ [[https://github.com/syl20bnr/spacemacs][https://cdn.rawgit.com/syl20bnr/spacemacs/442d025779da2f62fc86c2082703697714db6514/assets/spacemacs-badge.svg]] [[https://circleci.com/gh/tom-and-the-toothfairies/pathways][https://img.shields.io/circleci/project/github/tom-and-the-toothfairies/pathways.svg]] ** Installation This project includes a Docker Compose file for easy installation and testing. -Installation instructions for Docker on your platform can be found [[https://www.docker.com/products/docker][here]]. +Installation instructions for Docker on your platform can be found [[https://www.docker.com/community-edition#/download][here]]. For Docker installations on Ubuntu ~sudo~ is required to run Docker and Docker Compose commands. You will also need to install ~docker-compose~ separately with @@ -15,7 +15,7 @@ If you do not wish to build the Docker containers yourself you can run it directly from the Docker registry. First, install the necessary Docker components for your operating system as outlined above. -Then you can run the latest release from [[https://hub.docker.com/r/tomtoothfairies/pathways/][Docker Hub]] +Then you can run the latest release from [[https://hub.docker.com/u/tomtoothfairies/][Docker Hub]] #+BEGIN_SRC bash $ docker-compose up -d #+END_SRC @@ -28,7 +28,7 @@ $ docker-compose down To run a release other than the current you may manually checkout the release tag and run ~docker-compose~ #+BEGIN_SRC bash -$ git checkout 0.2 +$ git checkout 0.3 $ docker-compose up -d #+END_SRC @@ -55,6 +55,7 @@ $ sudo service docker restart $ cd pathways $ docker build -t tomtoothfairies/asclepius asclepius $ docker build -t tomtoothfairies/panacea panacea + $ docker built -t tomtoothfairies/chiron chiron #+END_SRC 3) Run the tests #+BEGIN_SRC bash @@ -63,156 +64,26 @@ $ sudo service docker restart #+END_SRC ** Features -At the time of writing - iteration 2 - the following features are implemented -and testable. Continuous integration testing has been set up for the project and -can be tracked [[https://circleci.com/gh/tom-and-the-toothfairies/pathways][here]]. -*** PML - Panacea -The PML and DINTO features are split into to separate services. The PML -service - Panacea - is responsible for the user interface and the analysis of -PML. The user interface is a web application. With the project running: -#+BEGIN_SRC bash -$ docker-compose up -d -#+END_SRC -the interface is accessible at ~http://localhost:4000~. The automated tests for -the PML service can be found in ~panacea/test~. To run the tests locally: -#+BEGIN_SRC bash -$ docker run -t -e "MIX_ENV=test" tomtoothfairies/panacea mix test -#+END_SRC -**** PML File Selection -The home page of the application contains a file selector. To test this feature, -click the ~choose file~ button and you can then browse the file system and -select a PML file. -**** PML File Loading -Once a file has been selected, it can be loaded into the system by clicking the -~Submit~ button. -**** Running PML Analysis -When a file is submitted, it is sent to the Panacea server for analysis. The -file's type and encoding are verified, then the files contents are run through a -lexer and parser to ensure the PML is well structured. This feature can be -tested manually by selecting and uploading a file. Some sample PML files can be -found in the ~panacea/test/fixtures~ directory. There are also extensive unit -tests for the components of this feature's implementation. - -Automated tests ensure that the lexer and parser can correctly parse all of John -Noll's sample pml from the PEOS project, and reject bad PML. -**** PML Error & Warning Highlights -The results of the analysis are sent back to the user interface and presented to -the user. This feature can be tested manually by uploading files for analysis. -In ~panacea/test/fixtures~ there files to cover each of the possible cases. -~bad.pml~ is a PML file that is not well-structured. ~drugs.pml~ contains -well-structured PML. There is also PNG which can be submitted to verify that the -system rejects non UTF-8 encoded files. - -Automated tests exists to exercise each of these possible cases and can be found -in: ~panacea/test/controllers/pml_controller_test.exs~. -**** Identifying Drugs in PML -From our investigations, CHEBI and DINTO identifiers seem to be the simplest way -to easily identify a drug. These identifiers take the form ~chebi:\d+~ or -~dinto:DB\d+~ where ~\d+~ is any sequence of digits. As such, the lexer and -parser will identify any string in this format as a drug. A drug that is to be -administered to a patient should be placed in a ~requires~ block. - -When a PML file is successfully analysed, any drugs found in ~requires~ blocks -are reported back to the user as 'identified drugs'. - -Again, this feature can be manually tested by uploading a file. -~panacea/test/fixtures/drugs.pml~ is a well-structured PML document containing -some drug identifiers. - -Automated tests for the parser and web interface also ensure that this feature -works as intended. - -*** DINTO - Asclepius ⚕ - Asclepius provides an endpoint for querying DINTO. It supports querying for all drugs listed within its given Ontology, as well as finding all, or specific drug-drug interactions. -**** Setup -***** DINTO Ontology Selection -By default, the application uses ~DINTO/DINTO 1/DINTO_1.owl~ as its ontology. -This can be overriden by setting the ~ASCLEPIUS_ONTOLOGY_FILE~ environment variable. - -*Note*: As the ontology is rather large, startup can take upwards of 3 minutes. - -**** Endpoints -***** ~/ping~ -| Description | Check endpoint availability | -| Methods | ~GET~ | -| Parameters | None | -| Returns | HTTP 204 (No Content) | - -***** ~/all_drugs~ -| Description | Find all drugs in the DINTO ontology | -| Methods | ~GET~ | -| Parameters | None | -| Returns | A list containing pairs of the canonical URI for a drug, as well as its English Label | - -****** Example -******* Response Body (Truncated) -#+BEGIN_SRC json -[ - { - "label": "carbapenem MM22383", - "uri": "http://purl.obolibrary.org/obo/CHEBI_58998" - }, - { - "label": "adenosine-5'-ditungstate", - "uri": "http://purl.obolibrary.org/obo/DINTO_DB02183" - }, - { - "label": "(5z)-13-chloro-14,16-dihydroxy-3,4,7,8,9,10-hexahydro-1h-2-benzoxacyclotetradecine-1,11(12h)-dione", - "uri": "http://purl.obolibrary.org/obo/DINTO_DB08346" - }, - { - "label": "etoposide", - "uri": "http://purl.obolibrary.org/obo/CHEBI_4911" - } -] -#+END_SRC - -***** ~/all_ddis~ -| Description | Find all drug-drug interactions (DDIs) in the DINTO ontology | -| Methods | ~GET~ | -| Parameters | None | -| Returns | A list containing pairs of the canonical URI for a drug-drug interaction, as well as its English Label | - -****** Example -******* Response Body (Truncated) -#+BEGIN_SRC json -[ - { - "label": "torasemide/trandolapril DDI", - "uri": "http://purl.obolibrary.org/obo/DINTO_11031" - }, - { - "label": "cimetidine/heroin DDI", - "uri": "http://purl.obolibrary.org/obo/DINTO_02733" - }, - { - "label": "methylergonovine/telithromycin DDI", - "uri": "http://purl.obolibrary.org/obo/DINTO_10154" - } -] -#+END_SRC - -***** ~/ddis~ -| Description | Find all drug-drug interactions (DDI) in the DINTO ontology which involve only the /given/ drugs | -| Methods | ~POST~ | -| Request Body | An object containing a list of /drug references/, named ~drugs~, where a /drug reference/ is either ~dinto:DB123~ or ~chebi:123~ | -| Returns | A list containing pairs of the canonical URI for a drug-drug interaction, as well as its English Label | - -****** Example -******* Request Body - #+BEGIN_SRC json -{"drugs": ["chebi:421707", "chebi:465284", "dinto:DB00503", "chebi:9342"]} - #+END_SRC -******* Response Body - #+BEGIN_SRC json -[ - { - "label": "abacavir/ganciclovir DDI", - "uri": "http://purl.obolibrary.org/obo/DINTO_05759" - }, - { - "label": "abacavir/ritonavir DDI", - "uri": "http://purl.obolibrary.org/obo/DINTO_11043" - } -] - #+END_SRC +The feature list for the project can be found [[./FEATURES.md][here]]. +** Change Log +The project's change log can be found [[./CHANGELOG.md][here]] +** Architecture Overview +The system is split into three distinct services, Pancea, Asclepius and Chiron. +They each run inside a docker container. The containers can be easily managed +using ~docker-compose~ as mentioned earlier. +*** Panacea +Panacea is responsible for the UI and PML analysis. It is a web application that +serves the UI and exposes an API for uploading PML files for analysis. + +More information about Panacea can be found [[./panacea/README.md][here]]. +*** Chiron +Chiron houses the DINTO data. The data is compiled into a triple store, Chiron +exposes a HTTP API for querying the triple store. + +More information about Chiron can be found [[./chiron/REAME.md][here]]. +*** Asclepius +Asclepius acts as an intermediary between Panacea and Chiron. It accepts +requests to identify DDIs from Panacea, creates the necessary SPARQL query and +passes it on to Chiron. + +More information about Asclepius can be found [[./asclepius/README.org][here]]. diff --git a/asclepius/.dockerignore b/asclepius/.dockerignore index 482bf71b..3a2d34a9 100644 --- a/asclepius/.dockerignore +++ b/asclepius/.dockerignore @@ -1,4 +1,5 @@ **/__pycache__ **/*.pcy -env -DINTO \ No newline at end of file +/env +DINTO +/.cache diff --git a/asclepius/Dockerfile b/asclepius/Dockerfile index 1ef1838e..0e19627f 100644 --- a/asclepius/Dockerfile +++ b/asclepius/Dockerfile @@ -5,20 +5,17 @@ MAINTAINER Eoin Houlihan EXPOSE 5000 ENV PYTHONPATH=asclepius -ENV FLASK_APP=asclepius/main.py +ENV FLASK_CONFIGURATION=production -RUN apk add --update git +RUN apk --no-cache add git gcc musl-dev RUN mkdir -p /opt/app WORKDIR /opt/app -# Cache DINTO -RUN git clone https://github.com/labda/DINTO.git && cd DINTO && git checkout 68a29b5 - # Cache python deps COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY . . -CMD ["flask", "run", "--host", "0.0.0.0"] +CMD ["gunicorn", "-w", "9", "-b", ":5000", "-k", "eventlet", "--log-config", "asclepius/gunicorn_logging.conf", "main:app"] diff --git a/asclepius/README.org b/asclepius/README.org index b8893cf4..2d950685 100644 --- a/asclepius/README.org +++ b/asclepius/README.org @@ -1,20 +1,11 @@ * Asclepius ⚕ -Flask endpoint for querying DINTO. Supports querying for all drugs listed, as well as finding all, or specific drug-drug interactions. +Flask endpoint for querying DINTO. Supports querying for all drugs listed, as +well as finding all, or specific drug-drug interactions. -** Setup -*** DINTO Ontology Selection -By default, the application uses ~DINTO/DINTO 1/DINTO_1.owl~ as its ontology. -This can be overriden by setting the ~ASCLEPIUS_ONTOLOGY_FILE~ environment variable. - -*Note*: As the ontology is rather large, startup can take upwards of 3 minutes. +This application acts as an adaptor to Chiron - an instance of Apache Fuseki, +Chiron must be running before any queries can be served. ** Endpoints -*** ~/ping~ -| Description | Check endpoint availability | -| Methods | ~GET~ | -| Parameters | None | -| Returns | HTTP 204 (No Content) | - *** ~/all_drugs~ | Description | Find all drugs in the DINTO ontology | | Methods | ~GET~ | @@ -73,23 +64,27 @@ This can be overriden by setting the ~ASCLEPIUS_ONTOLOGY_FILE~ environment varia | Description | Find all drug-drug interactions (DDI) in the DINTO ontology which involve only the /given/ drugs | | Methods | ~POST~ | | Request Body | An object containing a list of /drug references/, named ~drugs~, where a /drug reference/ is either ~dinto:DB123~ or ~chebi:123~ | -| Returns | A list containing pairs of the canonical URI for a drug-drug interaction, as well as its English Label | +| Returns | A list of DDI objects; its label, its URI, and the identifiers of the two drugs involved | **** Example ***** Request Body - #+BEGIN_SRC json +#+BEGIN_SRC json {"drugs": ["chebi:421707", "chebi:465284", "dinto:DB00503", "chebi:9342"]} - #+END_SRC +#+END_SRC ***** Response Body - #+BEGIN_SRC json +#+BEGIN_SRC json [ { + "drug_a": "chebi:421707", + "drug_b": "chebi:465284", "label": "abacavir/ganciclovir DDI", "uri": "http://purl.obolibrary.org/obo/DINTO_05759" }, { + "drug_a": "chebi:421707", + "drug_b": "dinto:DB00503", "label": "abacavir/ritonavir DDI", "uri": "http://purl.obolibrary.org/obo/DINTO_11043" } ] - #+END_SRC +#+END_SRC diff --git a/asclepius/asclepius/app.py b/asclepius/asclepius/app.py new file mode 100644 index 00000000..4fc7efa3 --- /dev/null +++ b/asclepius/asclepius/app.py @@ -0,0 +1,6 @@ +from flask import Flask + +from config import configure_app + +app = Flask(__name__) +configure_app(app) diff --git a/asclepius/asclepius/config.py b/asclepius/asclepius/config.py new file mode 100644 index 00000000..8eb00c8b --- /dev/null +++ b/asclepius/asclepius/config.py @@ -0,0 +1,32 @@ +import os + +class BaseConfig(object): + DEBUG = False + TESTING = False + FUSEKI_ADDRESS = 'localhost:3030' + + +class DevelopmentConfig(BaseConfig): + DEBUG = True + TESTING = False + + +class TestConfig(BaseConfig): + DEBUG = False + TESTING = True + +class ProductionConfig(BaseConfig): + DEBUG = False + TESTING = False + FUSEKI_ADDRESS = 'chiron:3030' + +config = { + "development": "config.DevelopmentConfig", + "test": "config.TestConfig", + "production": "config.ProductionConfig" +} + + +def configure_app(app): + config_name = os.getenv('FLASK_CONFIGURATION', 'development') + app.config.from_object(config[config_name]) diff --git a/asclepius/asclepius/dinto.py b/asclepius/asclepius/dinto.py index a0c0aea5..0de2e931 100644 --- a/asclepius/asclepius/dinto.py +++ b/asclepius/asclepius/dinto.py @@ -1,46 +1,116 @@ -import functools +import re +import requests import logging -import os +from functools import lru_cache -import rdflib +from app import app -import queries -from utils import timing +__all__ = ['all_drugs', 'all_ddis', 'ddi_from_drugs'] -__all__ = ['dinto'] +SPARQL_ADDRESS = app.config['FUSEKI_ADDRESS'] +SPARQL_ENDPOINT = f'http://{SPARQL_ADDRESS}/dinto/query' -ONTOLOGY_FILE = os.getenv('ASCLEPIUS_ONTOLOGY_FILE', 'DINTO/DINTO 1/DINTO_1.owl') +logging.info(f"Using Fuseki server at {SPARQL_ADDRESS}") - -class Dinto(): - def __init__(self, owl_filepath=ONTOLOGY_FILE): - self.graph = rdflib.Graph() - - with timing(f"loading ontology from {owl_filepath}"): - self.graph.load(owl_filepath) - - def _listify(self, query_result): - return [[y.toPython() for y in x] for x in query_result] - - @functools.lru_cache() - def all_drugs(self): - with timing(f"querying DINTO for all drugs"): - res = self.graph.query(queries.all_drugs()) - - return self._listify(res) - - def all_ddis(self): - with timing(f"querying DINTO for all DDIs"): - res = self.graph.query(queries.all_ddis()) - - return self._listify(res) - - @functools.lru_cache() - def ddi_from_drugs(self, drugs): - with timing(f"querying DINTO for drugs: {repr(drugs)}"): - res = self.graph.query(queries.ddi_from_drugs(drugs)) - - return self._listify(res) - - -dinto = Dinto() +DRUG_PATTERN = re.compile('(dinto:DB)|(chebi:)\d+') + +DINTO_PREFIX = 'http://purl.obolibrary.org/obo/DINTO_' +CHEBI_PREFIX = 'http://purl.obolibrary.org/obo/CHEBI_' + +PREFIXES = f''' +PREFIX rdf: +PREFIX rdfs: +PREFIX xsd: +PREFIX owl: +PREFIX dinto: <{DINTO_PREFIX}> +PREFIX chebi: <{CHEBI_PREFIX}> +''' + +PHARMACOLOGICAL_ENTITY = 'dinto:000055' +DDI = 'dinto:00010' + + +def sparql(qfunction): + """ + Cause a function which returns a sparql query to actually run that query. + Assumes that the arguments that the qfunction takes are all hashabled, to + make use of the handy-dandy lru cache transparently + """ + + def _do_sparql(query): + payload = {'query': query} + response = requests.post(SPARQL_ENDPOINT, data=payload) + + if response.status_code != 200: + response.raise_for_status() + else: + result = response.json() + + return [{v: entry[v]['value'] for v in result['head']['vars']} + for entry in result['results']['bindings']] + + @lru_cache() + def sparqled(*args, **kwargs): + query = qfunction(*args, **kwargs) + return _do_sparql(query) + + return sparqled + + +@sparql +def all_drugs(): + return f''' + {PREFIXES} + SELECT ?uri ?label + WHERE {{ + ?uri rdfs:subClassOf {PHARMACOLOGICAL_ENTITY}. + ?uri rdfs:label ?label + }} + ''' + + +@sparql +def all_ddis(): + return f''' + {PREFIXES} + SELECT ?uri ?label + WHERE {{ + ?uri rdfs:subClassOf {DDI}. + ?uri rdfs:label ?label + }} + ''' + + +def _valid_drug(drug_identifier): + return DRUG_PATTERN.match(drug_identifier) is not None + + +@sparql +def ddi_from_drugs(drugs): + if not isinstance(drugs, frozenset): + raise ValueError("for cachability, `drugs` must be given as a frozenset") + + if len(drugs) < 2: + raise ValueError("Need at least 2 drugs to find interactions") + + if not all(_valid_drug(drug) for drug in drugs): + raise ValueError("Drugs must be specified as chebi:123 or dinto:DB123") + + drug_search_space = ', '.join(drugs) + + return f''' + {PREFIXES} + SELECT ?drug_a ?drug_b ?uri ?label + WHERE {{ + ?uri rdfs:label ?label. + ?uri rdfs:subClassOf {DDI} . + ?uri owl:equivalentClass ?equivalance . + ?equivalance owl:intersectionOf ?restrictions . + ?restrictions rdf:first ?drug1Restriction . + ?restrictions rdf:rest ?tail . + ?tail rdf:first ?drug2Restriction . + ?drug1Restriction owl:someValuesFrom ?drug_a . + ?drug2Restriction owl:someValuesFrom ?drug_b . + FILTER (?drug_a in ({drug_search_space}) && + ?drug_b in ({drug_search_space}) ) + }}''' diff --git a/asclepius/asclepius/gunicorn_logging.conf b/asclepius/asclepius/gunicorn_logging.conf new file mode 100644 index 00000000..fb07ee36 --- /dev/null +++ b/asclepius/asclepius/gunicorn_logging.conf @@ -0,0 +1,46 @@ +[loggers] +keys=root, gunicorn.error, gunicorn.access, requests.packages.urllib3.connectionpool, __main__ + +[handlers] +keys=stdout + +[formatters] +keys=generic + +[logger_root] +level=INFO +handlers=stdout + +[logger___main__] +level=DEBUG +handlers=stdout +propagate=0 +qualname=__main__ + +[logger_gunicorn.error] +level=INFO +handlers=stdout +propagate=0 +qualname=gunicorn.error + +[logger_gunicorn.access] +level=INFO +handlers=stdout +propagate=0 +qualname=gunicorn.access + +[logger_requests.packages.urllib3.connectionpool] +level=WARN +handlers=stdout +propagate=0 +qualname=requests.packages.urllib3.connectionpool + +[handler_stdout] +class=StreamHandler +formatter=generic +args=(sys.stdout,) + +[formatter_generic] +format=%(asctime)s [%(process)d:%(name)s:%(lineno)s] [%(levelname)s] %(message)s +datefmt=%Y-%m-%d %H:%M:%S +class=logging.Formatter \ No newline at end of file diff --git a/asclepius/asclepius/main.py b/asclepius/asclepius/main.py index 6a649743..7db9528d 100644 --- a/asclepius/asclepius/main.py +++ b/asclepius/asclepius/main.py @@ -1,12 +1,12 @@ import logging -logging.basicConfig(level=logging.DEBUG) +logging.basicConfig(level=logging.INFO) -from flask import Flask, jsonify, request +from flask import jsonify, request +from app import app -from dinto import dinto +import dinto -app = Flask(__name__) class InvalidUsage(Exception): @@ -32,29 +32,23 @@ def handle_invalid_usage(error): return response -def to_labelled(query_result): - return [{'uri': uri, 'label': label} for (uri, label) in query_result] - - @app.route("/all_drugs", methods=['GET']) -def drugs(): +def all_drugs(): """Return a list of all drugs listed in DINTO""" - return jsonify(to_labelled(dinto.all_drugs())) + return jsonify(dinto.all_drugs()) @app.route("/all_ddis", methods=['GET']) def all_ddis(): """Return a list of all drug-drug interactions listed identified in DINTO""" - return jsonify(to_labelled(dinto.all_ddis())) + return jsonify(dinto.all_ddis()) @app.route('/ddis', methods=['POST']) def ddis(): """Return all of the Drug-Drug interactions involving the given list of (at least 2) drugs - post parameters: drugs: [, , ... ]] - Drugs are identified as either 'dinto:DB123' or 'chebi:123'""" params = request.get_json() @@ -69,14 +63,15 @@ def ddis(): except ValueError as e: raise InvalidUsage(str(e)) - return jsonify(to_labelled(dinto_res)) - + for ddi in dinto_res: + for drug in ('drug_a', 'drug_b'): + if ddi[drug].startswith(dinto.DINTO_PREFIX): + ddi[drug] = ddi[drug].replace(dinto.DINTO_PREFIX, 'dinto:') + elif ddi[drug].startswith(dinto.CHEBI_PREFIX): + ddi[drug] = ddi[drug].replace(dinto.CHEBI_PREFIX, 'chebi:') -@app.route("/ping") -def ping(): - return '', 204 + return jsonify(dinto_res) if __name__ == '__main__': - logging.info('Finished') - app.run(debug=False, host='0.0.0.0') + app.run() diff --git a/asclepius/asclepius/queries.py b/asclepius/asclepius/queries.py deleted file mode 100644 index a3bb82f8..00000000 --- a/asclepius/asclepius/queries.py +++ /dev/null @@ -1,77 +0,0 @@ -from rdflib.plugins.sparql import prepareQuery -import re - -__all__ = ['all_drugs', 'all_ddis', 'ddi_from_drugs'] - -PREFIXES = ''' -PREFIX rdf: -PREFIX rdfs: -PREFIX xsd: -PREFIX dinto: -PREFIX owl: -PREFIX chebi: -''' - -PHARMACOLOGICAL_ENTITY = 'dinto:000055' -DDI = 'dinto:00010' - -def all_drugs(): - q = f''' - {PREFIXES} - SELECT ?drug ?label - WHERE {{ - ?drug rdfs:subClassOf {PHARMACOLOGICAL_ENTITY}. - ?drug rdfs:label ?label - }} - ''' - return prepareQuery(q) - -def all_ddis(): - q = f''' - {PREFIXES} - SELECT ?interaction ?label - WHERE {{ - ?interaction rdfs:subClassOf {DDI}. - ?interaction rdfs:label ?label - }} - ''' - return prepareQuery(q) - - -def _valid_drug(drug_identifier): - pattern = re.compile('(dinto:DB)|(chebi:)\d+') - return pattern.match(drug_identifier) is not None - -def ddi_from_drugs(drugs): - if len(drugs) < 2: - raise ValueError("Need at least 2 drugs to find interactions") - - if not all(_valid_drug(drug) for drug in drugs): - raise ValueError("Drugs must be specified as chebi:123 or dinto:DB123") - - - drug_search_space = ', '.join(drugs) - - q = f''' - {PREFIXES} - SELECT ?ddi ?label - WHERE {{ - ?ddi rdfs:label ?label. - ?ddi rdfs:subClassOf {DDI} . - - ?ddi owl:equivalentClass ?equivalance . - ?equivalance owl:intersectionOf ?restrictions . - - ?restrictions rdf:first ?drug1Restriction . - ?restrictions rdf:rest ?tail . - - ?tail rdf:first ?drug2Restriction . - - ?drug1Restriction owl:someValuesFrom ?drug_a . - ?drug2Restriction owl:someValuesFrom ?drug_b . - - FILTER (?drug_a in ({drug_search_space}) && - ?drug_b in ({drug_search_space}) ) - - }}''' - return prepareQuery(q) diff --git a/asclepius/asclepius/utils.py b/asclepius/asclepius/utils.py deleted file mode 100644 index 6419c28d..00000000 --- a/asclepius/asclepius/utils.py +++ /dev/null @@ -1,15 +0,0 @@ -from timeit import default_timer as timer -from contextlib import contextmanager -import logging - -__all__ = ['timing'] - - -@contextmanager -def timing(description): - logging.info(f"Timing {description}") - start = timer() - yield - end = timer() - duration = end - start - logging.info(f"Finished {description} ({duration:0.3f} s)") diff --git a/asclepius/requirements.txt b/asclepius/requirements.txt index 50f01fe6..4eab383b 100644 --- a/asclepius/requirements.txt +++ b/asclepius/requirements.txt @@ -1,11 +1,13 @@ click==6.7 +enum-compat==0.0.2 +eventlet==0.20.1 Flask==0.12 -isodate==0.5.4 +greenlet==0.4.12 +gunicorn==19.7.0 itsdangerous==0.24 Jinja2==2.9.5 MarkupSafe==0.23 py==1.4.32 -pyparsing==2.1.10 pytest==3.0.6 -rdflib==4.2.2 +requests==2.13.0 Werkzeug==0.11.15 diff --git a/asclepius/test/test_queries.py b/asclepius/test/test_queries.py index c5e8e6de..df0dd606 100644 --- a/asclepius/test/test_queries.py +++ b/asclepius/test/test_queries.py @@ -1,10 +1,7 @@ import pytest -from rdflib.plugins.sparql.sparql import Query -from asclepius.queries import ( +from asclepius.dinto import ( _valid_drug, - all_drugs, - all_ddis, ddi_from_drugs, ) @@ -16,19 +13,12 @@ def test_valid_drug(): assert not _valid_drug('chebi:DB123') -def test_all_ddis_compiles(): - assert type(all_ddis()) == Query - - -def test_all_drugs_compiles(): - assert type(all_drugs()) == Query - - -def test_ddi_from_drugs_compiles_with_correct_drug_identifiers(): - assert type(ddi_from_drugs(['dinto:DB123', 'chebi:123'])) == Query - - def test_ddi_from_drugs_raises_with_incorrect_drug_identifiers(): with pytest.raises(ValueError): - ddi_from_drugs(['dinto:123', 'chebi:123']) - ddi_from_drugs(['garbage', 'rubbish']) + ddi_from_drugs(frozenset([])) + + with pytest.raises(ValueError): + ddi_from_drugs(frozenset(['garbage', 'rubbish'])) + + with pytest.raises(TypeError): + ddi_from_drugs(['dinto:db123', 'chebi:123']) diff --git a/chiron/Dockerfile b/chiron/Dockerfile new file mode 100644 index 00000000..5ad71e8f --- /dev/null +++ b/chiron/Dockerfile @@ -0,0 +1,40 @@ +FROM anapsix/alpine-java:jre8 +MAINTAINER Conor Brennan + +EXPOSE 3030 +RUN apk add --no-cache git curl + +# Download tdbloader2 binary +ENV JENA_ARCHIVE https://archive.apache.org/dist/ +ENV JENA_VERSION 3.2.0 +ENV JENA_DIR /opt/jena + +RUN mkdir $JENA_DIR && \ + curl $JENA_ARCHIVE/jena/binaries/apache-jena-$JENA_VERSION.tar.gz | tar zxf - -C $JENA_DIR +ENV PATH $JENA_DIR/apache-jena-$JENA_VERSION/bin:$PATH + +# Download fuseki-server binary +ENV FUSEKI_VERSION 2.4.0 +ENV FUSEKI_DIR /opt/fuseki +ENV FUSEKI_HOME $FUSEKI_DIR/apache-jena-fuseki-$FUSEKI_VERSION + +RUN mkdir $FUSEKI_DIR && \ + curl $JENA_ARCHIVE/jena/binaries/apache-jena-fuseki-$FUSEKI_VERSION.tar.gz | tar zxf - -C $FUSEKI_DIR +ENV PATH $FUSEKI_DIR/apache-jena-fuseki-$FUSEKI_VERSION:$PATH + +# Checkout our chosen version of DINTO +ENV DINTO_DIR /opt/DINTO +ENV DINTO_SHA 68a29b5 + +WORKDIR $DINTO_DIR +RUN git clone https://github.com/labda/DINTO.git && \ + cd DINTO && \ + git checkout $DINTO_SHA && \ + mv DINTO\ 1/DINTO_1.owl $DINTO_DIR + +# Create indexed database from DINTO OWL +RUN SORT_ARGS="-m" tdbloader2 --loc $DINTO_DIR/tdb DINTO_1.owl && rm -rf DINTO DINTO_1.owl + + + +CMD ["fuseki-server", "--loc=/opt/DINTO/tdb", "/dinto"] diff --git a/chiron/README.md b/chiron/README.md new file mode 100644 index 00000000..5d78287b --- /dev/null +++ b/chiron/README.md @@ -0,0 +1,8 @@ +# Chiron + +This service is an instance +of [Apache Jena Fuseki](https://jena.apache.org/index.html). It stores DINTO's +owl in a triple store and provides a HTTP API for querying the store using +SPARQL. + +Asclepius and Panacea depend on this service. diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 6426f672..9d4f4762 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -3,8 +3,15 @@ version: '2' services: asclepius: image: tomtoothfairies/asclepius:latest + ports: + - '5000:5000' panacea: image: tomtoothfairies/panacea:latest ports: - '4000:4000' + + chiron: + image: tomtoothfairies/chiron:latest + ports: + - '3030:3030' diff --git a/docker-compose.yml b/docker-compose.yml index f8fce8ab..19cd311f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,9 +2,12 @@ version: '2' services: asclepius: - image: tomtoothfairies/asclepius:0.2 + image: tomtoothfairies/asclepius:0.3 panacea: - image: tomtoothfairies/panacea:0.2 + image: tomtoothfairies/panacea:0.3 ports: - '4000:4000' + + chiron: + image: tomtoothfairies/chiron:latest diff --git a/panacea/README.md b/panacea/README.md index 1dd4c404..c3881305 100644 --- a/panacea/README.md +++ b/panacea/README.md @@ -1,4 +1,5 @@ # Panacea -Service responsible for the UI and PML analysis. See the main README for a list -of implemented features and testing instructions. +Service responsible for the UI and PML analysis. The UI is a web app available +at [localhost:4000](localhost:4000). Panacea depends on both Asclepius and +Chiron to provide DDI analysis, these must be running for Panacea to work. diff --git a/panacea/config/config.exs b/panacea/config/config.exs index 68f576b9..16d0bd02 100644 --- a/panacea/config/config.exs +++ b/panacea/config/config.exs @@ -19,7 +19,7 @@ config :logger, :console, metadata: [:request_id] config :panacea, :asclepius, - uri: URI.parse("http://asclepius:5000"), + uri: URI.parse("http://localhost:5000"), api: Panacea.Asclepius.Remote.HTTP # Import environment specific config. This must remain at the bottom diff --git a/panacea/config/prod.exs b/panacea/config/prod.exs index 5c857bf9..ab5a2e5c 100644 --- a/panacea/config/prod.exs +++ b/panacea/config/prod.exs @@ -19,6 +19,9 @@ config :panacea, Panacea.Endpoint, # Do not print debug messages in production config :logger, level: :info +config :panacea, :asclepius, + uri: URI.parse("http://asclepius:5000") + # ## SSL Support # # To get SSL working, you will need to add the `https` key diff --git a/panacea/lib/panacea.ex b/panacea/lib/panacea.ex index 54046e7f..28e46957 100644 --- a/panacea/lib/panacea.ex +++ b/panacea/lib/panacea.ex @@ -6,7 +6,6 @@ defmodule Panacea do children = [ supervisor(Panacea.Endpoint, []), - supervisor(Panacea.Asclepius.Supervisor, []) ] opts = [strategy: :one_for_one, name: Panacea.Supervisor] diff --git a/panacea/lib/panacea/asclepius.ex b/panacea/lib/panacea/asclepius.ex index a9237598..a1b30a85 100644 --- a/panacea/lib/panacea/asclepius.ex +++ b/panacea/lib/panacea/asclepius.ex @@ -1,17 +1,5 @@ defmodule Panacea.Asclepius do - @name __MODULE__ + @asclepius_api Keyword.get(Application.get_env(:panacea, :asclepius), :api) - def ready? do - Agent.get(@name, &(&1)) - end - - def set_readiness(readiness) do - Agent.update(@name, fn _ -> readiness end) - end - - # Agent API - - def start_link do - Agent.start_link(fn -> false end, name: @name) - end + def ddis(drugs), do: @asclepius_api.ddis(drugs) end diff --git a/panacea/lib/panacea/asclepius/health_checker.ex b/panacea/lib/panacea/asclepius/health_checker.ex deleted file mode 100644 index ae529b99..00000000 --- a/panacea/lib/panacea/asclepius/health_checker.ex +++ /dev/null @@ -1,41 +0,0 @@ -defmodule Panacea.Asclepius.HealthChecker do - alias Panacea.Asclepius - use GenServer - require Logger - - @name __MODULE__ - @check_interval 5000 - - def start_link do - GenServer.start_link(__MODULE__, [], name: @name) - end - - def init(_) do - schedule_check() - {:ok, :no_response} - end - - def handle_info(:check_health, :ok), do: {:noreply, :ok} - def handle_info(:check_health, _) do - health = check_health() - update_readiness(health) - schedule_check() - {:noreply, health} - end - - defp check_health() do - Logger.info("Pinging Asclepius") - {health, _} = Asclepius.Remote.ping() - health - end - - defp update_readiness(:ok) do - Logger.info("Asclepius is ready.") - Asclepius.set_readiness(true) - end - defp update_readiness(_), do: Logger.info("Asclepius is not responding.") - - defp schedule_check do - Process.send_after(self(), :check_health, @check_interval) - end -end diff --git a/panacea/lib/panacea/asclepius/remote.ex b/panacea/lib/panacea/asclepius/remote.ex index afb674ce..e8372f51 100644 --- a/panacea/lib/panacea/asclepius/remote.ex +++ b/panacea/lib/panacea/asclepius/remote.ex @@ -3,11 +3,5 @@ defmodule Panacea.Asclepius.Remote do @type reason :: any @type response :: any - @callback ping() :: {:ok, response} | {:error, reason} @callback ddis([drug]) :: {:ok, response} | {:error, reason} - - @asclepius_api Keyword.get(Application.get_env(:panacea, :asclepius), :api) - - def ping, do: @asclepius_api.ping() - def ddis(drugs), do: @asclepius_api.ddis(drugs) end diff --git a/panacea/lib/panacea/asclepius/remote/http.ex b/panacea/lib/panacea/asclepius/remote/http.ex index 2abca829..01666ff6 100644 --- a/panacea/lib/panacea/asclepius/remote/http.ex +++ b/panacea/lib/panacea/asclepius/remote/http.ex @@ -2,21 +2,12 @@ defmodule Panacea.Asclepius.Remote.HTTP do @behaviour Panacea.Asclepius.Remote @asclepius_uri Keyword.get(Application.get_env(:panacea, :asclepius), :uri) - @ping_timeout 50 - # one minute timeout for now - @ddi_timeout 60 * 1000 @default_headers [{"Content-Type", "application/json"}] - def ping do - asclepius_uri("/ping") - |> HTTPoison.get([recv_timeout: @ping_timeout]) - |> decode_response() - end - def ddis(drugs) do {:ok, body} = %{drugs: drugs} |> Poison.encode asclepius_uri("/ddis") - |> HTTPoison.post(body, @default_headers, [recv_timeout: @ddi_timeout]) + |> HTTPoison.post(body, @default_headers) |> decode_response() end diff --git a/panacea/lib/panacea/asclepius/remote/mock.ex b/panacea/lib/panacea/asclepius/remote/mock.ex index 44f85b6c..042c39dd 100644 --- a/panacea/lib/panacea/asclepius/remote/mock.ex +++ b/panacea/lib/panacea/asclepius/remote/mock.ex @@ -1,21 +1,22 @@ defmodule Panacea.Asclepius.Remote.Mock do @behaviour Panacea.Asclepius.Remote - def ping(), do: {:ok, nil} def ddis(_drugs) do - [ - %{ - "label" => "tranylcypromine/vilazodone DDI", - "uri" => "http://purl.obolibrary.org/obo/DINTO_08338" - }, - %{ - "label" => "penbutolol/methysergide DDI", - "uri" => "http://purl.obolibrary.org/obo/DINTO_07540" - }, - %{ - "label" => "drospirenone/heparin DDI", - "uri" => "http://purl.obolibrary.org/obo/DINTO_03086" - } - ] + {:ok, + [ + %{ + "drug_a" => "chebi:421707", + "drug_b" => "chebi:465284", + "label" => "abacavir/ganciclovir DDI", + "uri" => "http://purl.obolibrary.org/obo/DINTO_05759" + }, + %{ + "drug_a" => "chebi:421707", + "drug_b" => "dinto:DB00503", + "label" => "abacavir/ritonavir DDI", + "uri" => "http://purl.obolibrary.org/obo/DINTO_11043" + } + ] + } end end diff --git a/panacea/lib/panacea/asclepius/supervisor.ex b/panacea/lib/panacea/asclepius/supervisor.ex deleted file mode 100644 index fbe63990..00000000 --- a/panacea/lib/panacea/asclepius/supervisor.ex +++ /dev/null @@ -1,16 +0,0 @@ -defmodule Panacea.Asclepius.Supervisor do - use Supervisor - - def start_link do - Supervisor.start_link(__MODULE__, []) - end - - def init([]) do - children = [ - worker(Panacea.Asclepius, []), - worker(Panacea.Asclepius.HealthChecker, []) - ] - - supervise(children, strategy: :one_for_one) - end -end diff --git a/panacea/lib/panacea/pml/parser.ex b/panacea/lib/panacea/pml/parser.ex index 78b40431..a9caca63 100644 --- a/panacea/lib/panacea/pml/parser.ex +++ b/panacea/lib/panacea/pml/parser.ex @@ -1,5 +1,5 @@ defmodule Panacea.Pml.Parser do - alias Panacea.Pml.Parser.Error + alias Panacea.Pml.Parser.{Logger, Error} @type drug :: String.t @@ -24,9 +24,14 @@ defmodule Panacea.Pml.Parser do end defp to_result({:error, reason}) do - {:error, Error.format(reason)} + formatted = Error.format(reason) + Logger.error("PML Parsing error: #{formatted}") + + {:error, formatted} end defp to_result({:ok, drugs}) do + Logger.info("PML Parsing success: #{Enum.join(drugs, " ")}") + {:ok, drugs} end end diff --git a/panacea/lib/panacea/pml/parser/logger.ex b/panacea/lib/panacea/pml/parser/logger.ex new file mode 100644 index 00000000..131aadaf --- /dev/null +++ b/panacea/lib/panacea/pml/parser/logger.ex @@ -0,0 +1,17 @@ +defmodule Panacea.Pml.Parser.Logger do + require Logger + + def info(message) do + if should_log?() do + Logger.info(message) + end + end + + def error(message) do + if should_log?() do + Logger.error(message) + end + end + + defp should_log?, do: Mix.env in [:prod, :dev] +end diff --git a/panacea/test/controllers/pml_controller_test.exs b/panacea/test/controllers/pml_controller_test.exs index 9d2cb74c..aa04a74b 100644 --- a/panacea/test/controllers/pml_controller_test.exs +++ b/panacea/test/controllers/pml_controller_test.exs @@ -8,18 +8,25 @@ defmodule Panacea.PmlControllerTest do end describe "PmlController.upload/2" do + @tag :err_highlights + @tag :pml_loading test "raises an error when no file is provided", %{conn: conn} do assert_raise Phoenix.ActionClauseError, fn -> post conn, pml_path(conn, :upload) end end + @tag :err_highlights + @tag :pml_loading test "raises an error when the file is invalid", %{conn: conn} do assert_raise Phoenix.ActionClauseError, fn -> post conn, pml_path(conn, :upload), %{upload: %{file: "nonsense"}} end end + @tag :err_highlights + @tag :pml_analysis + @tag :pml_loading test "returns an error for malformed pml", %{conn: conn} do filename = "bad.pml" file_path = Path.join(@fixtures_dir, filename) @@ -31,6 +38,8 @@ defmodule Panacea.PmlControllerTest do assert response_body(conn) |> Map.get("message") =~ "syntax error" end + @tag :err_highlights + @tag :pml_loading test "returns an error for incorrect filetype", %{conn: conn} do filename = "example.png" file_path = Path.join(@fixtures_dir, filename) @@ -42,14 +51,44 @@ defmodule Panacea.PmlControllerTest do assert response_body(conn) |> Map.get("message") =~ "Invalid filetype" end + @tag :identify_drugs + @tag :pml_analysis + @tag :pml_loading test "identifies the drugs in correct pml", %{conn: conn} do - filename = "drugs.pml" + filename = "no_ddis.pml" file_path = Path.join(@fixtures_dir, filename) upload = %Plug.Upload{path: file_path, filename: filename} conn = post conn, pml_path(conn, :upload), %{upload: %{file: upload}} - assert response(conn, 200) =~ ~s("drugs":["chebi:1234","dinto:DB1234"]) + assert conn.status == 200 + assert response_body(conn) |> Map.get("drugs") == ["chebi:1234", "dinto:DB1234"] + end + + @tag :identify_ddis + @tag :pml_loading + test "identifies DDIs with the drugs from the pml", %{conn: conn} do + filename = "ddis.pml" + file_path = Path.join(@fixtures_dir, filename) + upload = %Plug.Upload{path: file_path, filename: filename} + + conn = post conn, pml_path(conn, :upload), %{upload: %{file: upload}} + + assert conn.status == 200 + assert response_body(conn) |> Map.get("ddis") == [ + %{ + "drug_a" => "chebi:421707", + "drug_b" => "chebi:465284", + "label" => "abacavir/ganciclovir DDI", + "uri" => "http://purl.obolibrary.org/obo/DINTO_05759" + }, + %{ + "drug_a" => "chebi:421707", + "drug_b" => "dinto:DB00503", + "label" => "abacavir/ritonavir DDI", + "uri" => "http://purl.obolibrary.org/obo/DINTO_11043" + } + ] end end end diff --git a/panacea/test/fixtures/ddis.pml b/panacea/test/fixtures/ddis.pml new file mode 100644 index 00000000..e0852430 --- /dev/null +++ b/panacea/test/fixtures/ddis.pml @@ -0,0 +1,32 @@ +process foo { + task bar { + action baz { + tool { "pills" } + script { "eat the pills" } + agent { "patient" } + requires { "chebi:9342" } + provides { "a cured patient" } + } + action baz2 { + tool { "pills" } + script { "eat the pills" } + agent { "patient" } + requires { "dinto:DB00503" } + provides { "a cured patient" } + } + action baz3 { + tool { "pills" } + script { "eat the pills" } + agent { "patient" } + requires { "chebi:465284" } + provides { "a cured patient" } + } + action baz4 { + tool { "pills" } + script { "eat the pills" } + agent { "patient" } + requires { "chebi:421707" } + provides { "a cured patient" } + } + } +} diff --git a/panacea/test/fixtures/drugs.pml b/panacea/test/fixtures/no_ddis.pml similarity index 100% rename from panacea/test/fixtures/drugs.pml rename to panacea/test/fixtures/no_ddis.pml diff --git a/panacea/test/fixtures/no_drugs.pml b/panacea/test/fixtures/no_drugs.pml new file mode 100644 index 00000000..9d987977 --- /dev/null +++ b/panacea/test/fixtures/no_drugs.pml @@ -0,0 +1,45 @@ +process PSP { + task PSP_Meta_Action { + + iteration + { + + action Start_time_to_TRL manual + { + input {"TRL_Line_entry"} + output {"TRL_Line_entry"} + agent {"programmer"} + tool {"HTML"} + script {"TRL_Instructions"} + } + + + action Do_part_of_Action manual + { + input {"'Action'.inputs"} + output {"'Action'.outputs"} + agent {"'Action'.agent"} + tool {"'Action'.tool"} + script {"'Action'.script"} + } + + action Stop_and_duration_time_to_TRL manual + { + input {"TRL_Line_entry"} + output {"TRL_Line_entry"} + agent {"programmer"} + tool {"HTML"} + script {"TRL_Instructions" } + } + + } + + } +} + + + + + + + diff --git a/panacea/test/panacea/asclepius_test.exs b/panacea/test/panacea/asclepius_test.exs index 80e0029d..1ed81391 100644 --- a/panacea/test/panacea/asclepius_test.exs +++ b/panacea/test/panacea/asclepius_test.exs @@ -1,26 +1,3 @@ defmodule Panacea.AsclepiusTest do - alias Panacea.Asclepius use ExUnit.Case - - setup do - Asclepius.set_readiness(false) - end - - describe "ready?" do - test "defaults to false" do - refute Asclepius.ready? - end - end - - describe "set_readiness" do - test "updates the readiness" do - refute Asclepius.ready? - - Asclepius.set_readiness(true) - assert Asclepius.ready? - - Asclepius.set_readiness(false) - refute Asclepius.ready? - end - end end diff --git a/panacea/test/panacea/pml/parser/error_test.exs b/panacea/test/panacea/pml/parser/error_test.exs index 96904be8..b5d7c27e 100644 --- a/panacea/test/panacea/pml/parser/error_test.exs +++ b/panacea/test/panacea/pml/parser/error_test.exs @@ -3,6 +3,7 @@ defmodule Panacea.Pml.Parser.ErrorTest do alias Panacea.Pml.Parser.Error describe "format" do + @tag :err_highlights test "it turns a lexer error tuple into a nice string" do {:error, error, _} = "process ," |> to_charlist |> :pml_lexer.string @@ -10,6 +11,7 @@ defmodule Panacea.Pml.Parser.ErrorTest do assert error |> Error.format == "line 1 -- unrecognized token ','" end + @tag :err_highlights test "it turns a parser error tuple into a nice string" do {:ok, tokens, _} = "process { foo }" |> to_charlist |> :pml_lexer.string {:error, error} = tokens |> :pml_parser.parse diff --git a/panacea/test/panacea/pml/parser_test.exs b/panacea/test/panacea/pml/parser_test.exs index c624810b..5eb74e96 100644 --- a/panacea/test/panacea/pml/parser_test.exs +++ b/panacea/test/panacea/pml/parser_test.exs @@ -6,6 +6,7 @@ defmodule Panacea.Pml.ParserTest do @fixtures_dir Path.join(~w(#{@root_dir} test fixtures jnolls_pml)) describe "parse/1" do + @tag :pml_analysis test "it parses correct pml" do pml = """ process foo { @@ -24,6 +25,7 @@ defmodule Panacea.Pml.ParserTest do assert Parser.parse(pml) == {:ok, []} end + @tag :pml_analysis test "it can parse all of jnoll's sample pml" do {:ok, files} = File.ls(@fixtures_dir) for file <- files do @@ -34,6 +36,8 @@ defmodule Panacea.Pml.ParserTest do end end + @tag :err_highlights + @tag :pml_analysis test "it rejects incorrect pml" do pml = """ process foo {{ @@ -42,6 +46,8 @@ defmodule Panacea.Pml.ParserTest do assert Parser.parse(pml) == {:error, "line 1 -- syntax error before: '{'"} end + @tag :identify_drugs + @tag :pml_analysis test "it identifies drugs" do pml = """ process foo { diff --git a/panacea/test/plugs/require_asclepius_test.exs b/panacea/test/plugs/require_asclepius_test.exs deleted file mode 100644 index 876564ca..00000000 --- a/panacea/test/plugs/require_asclepius_test.exs +++ /dev/null @@ -1,27 +0,0 @@ -defmodule Panacea.Plugs.RequireAsclepiusTest do - use ExUnit.Case - use Panacea.ConnCase - alias Panacea.Plugs.RequireAsclepius - - describe "when Asclepius is unavailable" do - test "it returns a 503", %{conn: conn} do - Panacea.Asclepius.set_readiness(false) - - conn = conn |> RequireAsclepius.call([]) - - assert conn.status == 503 - assert conn.halted - end - end - - describe "when asclepius is available" do - test "it does nothing", %{conn: conn} do - Panacea.Asclepius.set_readiness(true) - - new_conn = conn |> RequireAsclepius.call([]) - - assert new_conn == conn - refute new_conn.halted - end - end -end diff --git a/panacea/web/controllers/pml_controller.ex b/panacea/web/controllers/pml_controller.ex index afcb80ed..24320ab3 100644 --- a/panacea/web/controllers/pml_controller.ex +++ b/panacea/web/controllers/pml_controller.ex @@ -6,6 +6,7 @@ defmodule Panacea.PmlController do |> File.read |> validate |> parse + |> get_ddis |> respond(conn) end @@ -24,10 +25,18 @@ defmodule Panacea.PmlController do {:error, message} end - defp respond({:ok, drugs}, conn) do + defp get_ddis({:ok, drugs}) do + {:ok, ddis} = Panacea.Asclepius.ddis(drugs) + {:ok, %{drugs: drugs, ddis: ddis}} + end + defp get_ddis({:error, message}) do + {:error, message} + end + + defp respond({:ok, response}, conn) do conn |> put_status(:ok) - |> json(%{drugs: drugs}) + |> json(response) end defp respond({:error, message}, conn) do conn diff --git a/panacea/web/plugs/require_asclepius.ex b/panacea/web/plugs/require_asclepius.ex deleted file mode 100644 index e0bbc38e..00000000 --- a/panacea/web/plugs/require_asclepius.ex +++ /dev/null @@ -1,16 +0,0 @@ -defmodule Panacea.Plugs.RequireAsclepius do - alias Panacea.Asclepius - import Plug.Conn - - def init(opts), do: opts - - def call(conn, _opts) do - if Asclepius.ready? do - conn - else - conn - |> send_resp(:service_unavailable, "Asclepius is unavailable.") - |> halt() - end - end -end diff --git a/panacea/web/router.ex b/panacea/web/router.ex index 890a76dd..d1373dc7 100644 --- a/panacea/web/router.ex +++ b/panacea/web/router.ex @@ -13,10 +13,6 @@ defmodule Panacea.Router do plug :accepts, ["json"] end - pipeline :requires_asclepius do - plug Panacea.Plugs.RequireAsclepius, [] - end - scope "/", Panacea do pipe_through :browser # Use the default browser stack @@ -29,9 +25,5 @@ defmodule Panacea.Router do pipe_through :browser post "/pml", PmlController, :upload - - scope "/asclepius", Panacea do - pipe_through :requires_asclepius - end end end diff --git a/panacea/web/static/css/app.css b/panacea/web/static/css/app.css index fb213b10..9c795f97 100644 --- a/panacea/web/static/css/app.css +++ b/panacea/web/static/css/app.css @@ -5,6 +5,6 @@ margin-top: -10px; } -.panel-hidden { +.panel-hidden, #success { display: none; } diff --git a/panacea/web/static/js/app.js b/panacea/web/static/js/app.js index 13a7e01c..85896c30 100644 --- a/panacea/web/static/js/app.js +++ b/panacea/web/static/js/app.js @@ -25,12 +25,18 @@ async function submitFile() { } } -const successPanel = document.getElementById('success-panel'); +const successPanel = document.getElementById('success'); const errorPanel = document.getElementById('error-panel'); const renderFileResponse = data => { - const successResultMessage = document.getElementById('success-result-message'); - successResultMessage.innerHTML = JSON.stringify(data.drugs); + // Parsed drugs + const drugsResultMessage = document.getElementById('success-result-message'); + drugsResultMessage.innerHTML = JSON.stringify(data.drugs); + + // DDIS + const ddisResultMessage = document.getElementById('success-ddis-message'); + ddisResultMessage.innerHTML = JSON.stringify(data.ddis); + errorPanel.style.display = 'none'; successPanel.style.display = 'block'; }; diff --git a/panacea/web/templates/page/index.html.eex b/panacea/web/templates/page/index.html.eex index f42fcd50..26eebdb3 100644 --- a/panacea/web/templates/page/index.html.eex +++ b/panacea/web/templates/page/index.html.eex @@ -8,9 +8,15 @@ <%= submit "Submit", class: "btn btn-success btn-lg" %> <% end %> -
-
Drugs Found
-
+
+
+
Drugs Found
+
+
+
+
DDIs Found
+
+
PML Error