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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
.github
.git
.direnv
.idea
.eggs/
build
dist
*.egg
*.egg-info
tests
venv

__pycache__
*.pyo
*.pyd
*.output
.coverage
.coverage.*
.cache
htmlcov/
.pytest_cache
allure_report
CHANGELOG.md
29,218 changes: 0 additions & 29,218 deletions CHANGELOG.md

Large diffs are not rendered by default.

19 changes: 19 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
FROM python:3.10-slim-buster

LABEL MAINTAINER="Pradeep Bashyal"

WORKDIR /app

COPY requirements.txt /app
RUN pip install --no-cache-dir --upgrade pip && \
pip install --no-cache-dir -r requirements.txt

COPY requirements-deploy.txt /app
RUN pip install --no-cache-dir -r requirements-deploy.txt

COPY app.py /app/
COPY api.py /app/
COPY api-spec.yaml /app/
COPY pyard /app/pyard

CMD ["gunicorn", "--bind", "0.0.0.0:8080", "--worker-tmp-dir", "/dev/shm", "--timeout", "30", "app:app"]
6 changes: 3 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -95,18 +95,18 @@ dist: clean ## builds source and wheel package
ls -l dist

docker-build: ## build a docker image for the service
docker build -t my-project-template-service:0.0.1 .
docker build -t pyard-service:latest .

docker: docker-build ## build a docker image and run the service
docker run --name my-project-template -p 8080:8080 my-project-template-service:0.0.1
docker run --name pyard-service -p 8080:8080 pyard-service:latest

install: clean ## install the package to the active Python's site-packages
pip install --upgrade pip
python setup.py install
pip install -r requirements.txt
pip install -r requirements-tests.txt
pip install -r requirements-dev.txt
pip install -r requirements-deploy.txt
python setup.py install
pre-commit install

venv: ## creates a Python3 virtualenv environment in venv
Expand Down
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ ard.redux_gl('B14', 'lg')
| `lgx` | Reduce to 2 field ARD level |
| `W` | Reduce/Expand to 3 field WHO nomenclature level |
| `exon` | Reduce/Expand to exon level |
| `U2` | Reduce to 2 field unambiguous level |

# Command Line Tools

Expand Down Expand Up @@ -186,3 +187,19 @@ A*01:01/A*01:02/A*01:03/A*01:06/A*01:07/A*01:08/A*01:09/A*01:10/A*01:12/ ...
### Batch Reduce a CSV file

`pyard-csv-reduce` can be used to batch process a CSV file with HLA typings. See [documentation](extras/README.md) for instructions on how to configure and run.

## py-ard REST Service

Run `py-ard` as a service so that it can be accessed as a REST service endpoint.

Build the docker image:
```shell
make docker-build
```

Build the docker and run it with:
```shell
make docker
```

The endpoint should then be available at [localhost:8080](http://0.0.0.0:8080)
121 changes: 121 additions & 0 deletions api-spec.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
openapi: 3.0.3
info:
title: ARD Reduction
description: Reduce to ARD Level
version: "0.8.0"
servers:
- url: 'http://localhost:8080'
tags:
- name: ARD Reduction
description: Reduce GL String to ARD
paths:
/redux:
post:
tags:
- ARD Reduction
operationId: api.redux_controller
summary: Reduce to ARD
description: |
Given a GL String and a reduction method perform ARD Reduction
requestBody:
content:
application/json:
schema:
properties:
gl_string:
description: GL String
type: string
example: "A*01:01+A*01:01^B*08:ASXJP+B*07:02^C*02:02+C*07:HTGM^DPB1*28:01:01G+DPB1*296:01"
reduction_method:
description: Reduction Method
type: string
enum:
- G
- lg
- lgx
- W
- exon
- U2
example: "lgx"
responses:
200:
description: Reduction Result
content:
application/json:
schema:
type: object
properties:
ard:
description: ARD reduced version of GL String
type: string
example: "A*01:01+A*01:01^B*07:02+B*08:01^C*02:02/C*02:10+C*07:01/C*07:150Q^DPB1*28:01+DPB1*28:01"
400:
description: Invalid GL String Form
content:
application/json:
schema:
type: object
properties:
message:
description: Describes what went wrong
type: string
example: "Invalid HLA locus"
/validate:
post:
tags:
- ARD Reduction
operationId: api.validate_controller
summary: Validate GL String
description: |
Given a GL String report whether it is valid or not
requestBody:
content:
application/json:
schema:
properties:
gl_string:
description: GL String
type: string
example: "A*01:01+A*01:01^B*08:ASXJP+B*07:02^C*02:02+C*07:HTGM^DPB1*28:01:01G+DPB1*296:01"
responses:
200:
description: Validation Result
content:
application/json:
schema:
type: object
properties:
valid:
description: Is GL String valid
type: boolean
example: true
404:
description: GL String didn't validate
content:
application/json:
schema:
type: object
properties:
valid:
description: Is GL String valid
type: boolean
example: false
message:
description: Describes what went wrong
type: string
example: "Provided GL String is invalid: HLA-A*01:BLAH"
cause:
description: Explanation of why the GL String is not valid
type: string
example: "HLA-A*01:BLAH is not a valid GL Allele"
400:
description: Invalid GL String Form
content:
application/json:
schema:
type: object
properties:
message:
description: Describes what went wrong
type: string
example: "Invalid HLA locus"
47 changes: 47 additions & 0 deletions api.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
from flask import request

from pyard import ARD
from pyard.exceptions import PyArdError, InvalidAlleleError

# Globally accessible for all endpoints
ard = ARD()


def validate_controller():
if request.json:
# Check the request has required inputs
try:
gl_string = request.json["gl_string"]
except KeyError:
return {"message": "gl_string not provided"}, 404
# Validate
try:
ard.isvalid_gl(gl_string)
return {"valid": True}, 200
except InvalidAlleleError as e:
return {
"valid": False,
"message": f"Provided GL String is invalid: {gl_string}",
"cause": e.message,
}, 404
except PyArdError as e:
return {"message": e.message}, 400


def redux_controller():
if request.json:
# Check the request has required inputs
try:
gl_string = request.json["gl_string"]
reduction_method = request.json["reduction_method"]
except KeyError:
return {"message": "gl_string and reduction_method not provided"}, 404
# Perform redux
try:
redux_gl_string = ard.redux_gl(gl_string, reduction_method)
return {"ard": redux_gl_string}, 200
except PyArdError as e:
return {"message": e.message}, 400

# if no data is sent
return {"message": "No input data provided"}, 404
17 changes: 17 additions & 0 deletions app.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#!/usr/bin/env python3
import connexion

from flask import redirect

app = connexion.App(__name__)
application = app.app
api = app.add_api("api-spec.yaml")


@app.route("/")
def index():
return redirect(api.base_path + "/ui")


if __name__ == "__main__":
app.run(port=8080, debug=True)
7 changes: 4 additions & 3 deletions pyard/db.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,10 +57,11 @@ def create_db_connection(data_dir, imgt_version, ro=False):
if ro:
# Open the database in read-only mode
file_uri = f"file:{db_filename}?mode=ro"
else:
# Open the database in read-only mode
file_uri = f"file:{db_filename}"
# Multiple threads can access the same connection since it's only ro
return sqlite3.connect(file_uri, check_same_thread=False, uri=True)

# Open the database for read/write
file_uri = f"file:{db_filename}"
return sqlite3.connect(file_uri, uri=True)


Expand Down
3 changes: 2 additions & 1 deletion pyard/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,9 @@ def __str__(self) -> str:


class InvalidTypingError(PyArdError):
def __init__(self, message: str) -> None:
def __init__(self, message: str, cause=None) -> None:
self.message = message
self.cause = cause

def __str__(self) -> str:
return f"Invalid HLA Typing: {self.message}"
23 changes: 20 additions & 3 deletions pyard/pyard.py
Original file line number Diff line number Diff line change
Expand Up @@ -276,8 +276,7 @@ def redux_gl(self, glstring: str, redux_type: VALID_REDUCTION_TYPES) -> str:

validate_reduction_type(redux_type)

if not self.isvalid_gl(glstring):
raise InvalidTypingError(f"{glstring} is not a valid typing.")
self.validate(glstring)

if re.search(r"\^", glstring):
return self.sorted_unique_gl(
Expand Down Expand Up @@ -359,6 +358,21 @@ def redux_gl(self, glstring: str, redux_type: VALID_REDUCTION_TYPES) -> str:

return self.redux(glstring, redux_type)

def validate(self, glstring):
"""
Validates GL String
Raise an exception if not valid.

:param glstring: GL String to validate
:return: boolean indicating success
"""
try:
return self.isvalid_gl(glstring)
except InvalidAlleleError as e:
raise InvalidTypingError(
f"{glstring} is not valid GL String. \n {e.message}", e
)

def is_XX(self, glstring: str, loc_antigen: str = None, code: str = None) -> bool:
if loc_antigen is None or code is None:
if ":" in glstring:
Expand Down Expand Up @@ -603,7 +617,10 @@ def isvalid_gl(self, glstring: str) -> bool:
return all(map(self.isvalid_gl, glstring.split("/")))

# what falls through here is an allele
return self.isvalid(glstring)
is_valid_allele = self.isvalid(glstring)
if not is_valid_allele:
raise InvalidAlleleError(f"{glstring} is not a valid Allele")
return is_valid_allele

def mac_toG(self, allele: str) -> str:
"""
Expand Down
1 change: 1 addition & 0 deletions requirements-tests.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
behave==1.2.6
PyHamcrest==2.0.2
pytest==7.1.2
4 changes: 4 additions & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ replace = version="{new_version}"
search = __version__ = "{current_version}"
replace = __version__ = "{new_version}"

[bumpversion:file:api-spec.yaml]
search = version: "{current_version}"
replace = version: "{new_version}"

[bdist_wheel]
universal = 1

Expand Down