Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

enhancements for organs and dataset-types #132

Merged
merged 12 commits into from
Sep 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions .project
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>hs-ontology-api</name>
<comment></comment>
<projects>
<project>ubkg-api</project>
</projects>
<buildSpec>
<buildCommand>
<name>org.python.pydev.PyDevBuilder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.python.pydev.pythonNature</nature>
</natures>
</projectDescription>
13 changes: 13 additions & 0 deletions .pydevproject
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<?eclipse-pydev version="1.0"?><pydev_project>

<pydev_property name="org.python.pydev.PYTHON_PROJECT_VERSION">python interpreter</pydev_property>

<pydev_property name="org.python.pydev.PYTHON_PROJECT_INTERPRETER">Default</pydev_property>

<pydev_pathproperty name="org.python.pydev.PROJECT_SOURCE_PATH">
<path>/${PROJECT_DIR_NAME}/src</path>
<path>/${PROJECT_DIR_NAME}/dev</path>
</pydev_pathproperty>

</pydev_project>
2 changes: 2 additions & 0 deletions dev/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
#config file
instance/app.cfg
13 changes: 13 additions & 0 deletions dev/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
FROM hubmap/api-base-image:1.0.0

LABEL description="DEV Server for Assayclasses"

WORKDIR /usr/src/app

COPY . .

RUN pip install --upgrade pip -r requirements.txt

EXPOSE 8181

CMD [ "python", "-m" , "app"]
83 changes: 83 additions & 0 deletions dev/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
## Development AssayClasses RESTful service

The service contained within this directory exposes two endpoints:
- /assayclasses?application_context=HUBMAP
- assayclasses/<assay-code>?application_context=HUBMAP

To run this service, in this directory:
- copy instance/app.cfg.example to instance/app.cfg
- create a python virtual environment with the contents of requirements.txt imported into the environment
- source/activage the virtual environment
- execute `python app.py`
The service will be available on port 8181.

Both endpoints require the `application_context=HUBMAP` parameter. (A future version will allow SENNET context as well, which will read results from a different file).

The `/assayclasses` endpoint simply returns the contents of the file https://raw.githubusercontent.com/x-atlas-consortia/hs-ontology-api/dev-integrate/dev/assayclasses.json as a json response.

The `/assayclasses/<assay-code>` endpoint searches the same [assayclasses.json file](https://raw.githubusercontent.com/x-atlas-consortia/hs-ontology-api/dev-integrate/dev/assayclasses.json) for an assayclass item matching `rule_description.code` and returns the full matching assayclass item as a json response. If the code is not found a 404 is returned. For example if `/assayclasses/C200001?application_context=HUBMAP` is called the return value is:

```
{
"rule_description": {
"application_context": "HUBMAP",
"code": "C200150",
"name": "non-DCWG primary IMC2D"
},
"value": {
"active_status": "active",
"assaytype": "IMC2D",
"dataset_type": {
"PDR_category": "MxNF",
"dataset_type": "2D Imaging Mass Cytometry",
"fig2": {
"aggregated_assaytype": "LC-MS",
"category": "bulk",
"modality": "Proteomics"
}
},
"description": "2D Imaging Mass Cytometry",
"dir_schema": "imc-v0",
"is_multiassay": false,
"measurement_assay": {
"codes": [
{
"code": "SENNET:C006901",
"term": "Imaging Mass Cytometry Measurement Assay"
},
{
"code": "HUBMAP:C006901",
"term": "Imaging Mass Cytometry Measurement Assay"
},
{
"code": "OBI:0003096",
"term": "imaging mass cytometry assay"
}
],
"contains_full_genetic_sequences": false
},
"must_contain": [],
"pipeline_shorthand": null,
"process_state": "primary",
"provider": "IEC",
"tbl_schema": "imc-v",
"vitessce_hints": []
}
}
```

## Docker Deployment on DEV VM

First build a new docker image

```
docker compose build
```

Then spin up the container

```
docker compose up -d
```

Once the container is up running correctly, you can access at `http://gateway.dev.hubmapconsortium.org:8181/assayclasses`
125 changes: 125 additions & 0 deletions dev/app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
import os
import json
from flask import Flask, request, Response, abort
from urllib.request import urlopen
from urllib.error import HTTPError
import logging

#
# This is a RESTful service that exposes two endpoints:
# /assayclasses?application_context=HUBMAP
# /assayclasses/<assay-code>?application_context=HUBMAP
#
# Both endpoints require the application_context=HUBMAP parameter to be included, otherwise they return a 400.
#
# This service is intended to be for development purposes only, so a developer can easily control the output by
# changing the [assayclasses.json file in GitHub], specified in the ASSAYCLASSES_JSON_URL app.conf parameter.
#
# It is intended to mimic the services:
# https://ontology-api.dev.hubmapconsortium.org/assayclasses?application_context=HUBMAP
# https://ontology-api.dev.hubmapconsortium.org/assayclasses/C200150?application_context=HUBMAP
#
# Please see Issue: https://github.com/orgs/hubmapconsortium/projects/40/views/1?filterQuery=kollar&visibleFields=%5B%22Title%22%2C%22Assignees%22%2C%22Status%22%2C%22Labels%22%2C117184707%5D&pane=issue&itemId=74945308


logging.basicConfig(format='[%(asctime)s] %(levelname)s in %(module)s: %(message)s',
level=logging.DEBUG,
datefmt='%Y-%m-%d %H:%M:%S'
)
logger = logging.getLogger()


app = Flask(__name__,
instance_path=os.path.join(os.path.abspath(os.path.dirname(__file__)), 'instance'),
instance_relative_config=True)
app.config.from_pyfile('app.cfg')


def get_assayclasses() -> dict:
"""
Download the assayclasses information and return it as a dict.
Since this is done at startup time, on error it will log an error and exit as the app cannot recover from this.
"""
if "ASSAYCLASSES_JSON_URL" not in app.config:
logger.error("app.cfg must contain ASSAYCLASSES_JSON_URL")
exit(1)
assayclasses_json_url: str = app.config["ASSAYCLASSES_JSON_URL"]
try:
response = urlopen(assayclasses_json_url)
data: str = response.read().decode("utf-8")
except HTTPError as he:
logger.error(f"Could not read ASSAYCLASSES_JSON_URL: {assayclasses_json_url}, error: {he}")
exit(1)
try:
json_dict: dict = json.loads(data)
except json.JSONDecodeError as jde:
logger.error(f"Invalid JSON syntax: {jde}")
exit(1)
logger.info(f"successfully downloaded app.cfg:ASSAYCLASSES_JSON_URL={assayclasses_json_url} json as dict")
return json_dict


assayclasses_list: dict = get_assayclasses()


def find_assayclass_with_rule_description_code(code: str):
"""
Find the assayclass corresponding the rule_description code given or return None.
"""
for ad in assayclasses_list:
if "rule_description" in ad and "code" in ad["rule_description"] and ad["rule_description"]["code"] == code:
return ad
return None


def check_for_valid_application_context() -> None:
"""
Check for a valid auery parameter application_context, and if not found abort with a 400.
"""
application_context = request.args.get('application_context')
if application_context is None or application_context.upper() != "HUBMAP":
abort(Response(json.dumps({"message": "A query parameter of application_context=HUBMAP must be specified"}),
400,
mimetype='application/json'))


@app.route('/', methods=['GET'])
def index():
return "Hello! This is the DEV AssayClass service :)"


@app.route('/assayclasses/<code>', methods=['GET'])
def assayclasses_by_code(code):
"""
This endpoint searches the same assayclasses.json file for an assayclass item matching
rule_description.code and returns the full matching assayclass item as a json response.
If the code is not found a 404 is returned.
"""
check_for_valid_application_context()

assayclass_dict = find_assayclass_with_rule_description_code(code)

if assayclass_dict is not None:
return Response(json.dumps(assayclass_dict), 200, mimetype='application/json')

return Response(json.dumps({"message": f"No assayclass corresponding the rule_description code:{code} was found"}),
404, mimetype='application/json')


@app.route('/assayclasses', methods=['GET'])
def assayclass():
"""
This endpoint returns the contents of the ASSAYCLASSES_JSON_URL as a json response.
"""
check_for_valid_application_context()

return Response(json.dumps(assayclasses_list), 200, mimetype='application/json')


# For development/testing only
if __name__ == '__main__':
try:
port = 8181
app.run(port=port, host='0.0.0.0')
finally:
pass
Loading
Loading