diff --git a/apps/iatlas/api-gitlab/.coveragerc b/apps/iatlas/api-gitlab/.coveragerc new file mode 100644 index 0000000000..1125b12e9a --- /dev/null +++ b/apps/iatlas/api-gitlab/.coveragerc @@ -0,0 +1,16 @@ +[run] +branch = True +parallel = True +omit = + tests/* + config.py +command_line = -m pytest -n auto + +[report] +precision = 2 +sort = Cover + +[html] +directory = coverage +extra_css = coverage_assets/coverage.css +title = iAtlas API Test Coverage \ No newline at end of file diff --git a/apps/iatlas/api-gitlab/.devcontainer/devcontainer.json b/apps/iatlas/api-gitlab/.devcontainer/devcontainer.json new file mode 100644 index 0000000000..9091c65513 --- /dev/null +++ b/apps/iatlas/api-gitlab/.devcontainer/devcontainer.json @@ -0,0 +1,35 @@ +// For format details, see https://aka.ms/vscode-remote/devcontainer.json or this file's README at: +// https://github.com/microsoft/vscode-dev-containers/tree/v0.123.0/containers/docker-existing-docker-compose +// If you want to run as a non-root user in the container, see .devcontainer/docker-compose.yml. +{ + "name": "Existing Docker Compose (Extend)", + // Update the 'dockerComposeFile' list if you have more compose files or use different names. + // The .devcontainer/docker-compose.yml file contains any overrides you need/want to make. + "dockerComposeFile": [ + "../docker-compose.yml", + "docker-compose.yml" + ], + // The 'service' property is the name of the service for the container that VS Code should + // use. Update this value and .devcontainer/docker-compose.yml to the real service name. + "service": "api", + // The optional 'workspaceFolder' property is the path VS Code should open by default when + // connected. This is typically a file mount in .devcontainer/docker-compose.yml + "workspaceFolder": "/workspace", + "workspaceMount": "source=remote-workspace,target=/workspace,type=volume", + // Set *default* container specific settings.json values on container create. + "settings": { + "terminal.integrated.shell.linux": null + }, + // Add the IDs of extensions you want installed when the container is created. + "extensions": [] + // Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [], + // Uncomment the next line if you want start specific services in your Docker Compose config. + // "runServices": [], + // Uncomment the next line if you want to keep your containers running after VS Code shuts down. + // "shutdownAction": "none", + // Uncomment the next line to run commands after the container is created - for example installing curl. + // "postCreateCommand": "apt-get update && apt-get install -y curl", + // Uncomment to connect as a non-root user. See https://aka.ms/vscode-remote/containers/non-root. + // "remoteUser": "vscode" +} \ No newline at end of file diff --git a/apps/iatlas/api-gitlab/.devcontainer/docker-compose.yml b/apps/iatlas/api-gitlab/.devcontainer/docker-compose.yml new file mode 100644 index 0000000000..d3cd55d8c3 --- /dev/null +++ b/apps/iatlas/api-gitlab/.devcontainer/docker-compose.yml @@ -0,0 +1,43 @@ +#------------------------------------------------------------------------------------------------------------- +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. See https://go.microsoft.com/fwlink/?linkid=2090316 for license information. +#------------------------------------------------------------------------------------------------------------- + +version: '3.8' +services: + # Update this to the name of the service you want to work with in your docker-compose.yml file + api: + # If you want add a non-root user to your Dockerfile, you can use the "remoteUser" + # property in devcontainer.json to cause VS Code its sub-processes (terminals, tasks, + # debugging) to execute as the user. Uncomment the next line if you want the entire + # container to run as this user instead. Note that, on Linux, you may need to + # ensure the UID and GID of the container user you create matches your local user. + # See https://aka.ms/vscode-remote/containers/non-root for details. + # + # user: vscode + + # Uncomment if you want to override the service's Dockerfile to one in the .devcontainer + # folder. Note that the path of the Dockerfile and context is relative to the *primary* + # docker-compose.yml file (the first in the devcontainer.json "dockerComposeFile" + # array). The sample below assumes your primary file is in the root of your project. + # + # build: + # context: . + # dockerfile: .devcontainer/Dockerfile + + volumes: + # Update this to wherever you want VS Code to mount the folder of your project + - .:/workspace:cached + + # Uncomment the next line to use Docker from inside the container. See https://aka.ms/vscode-remote/samples/docker-from-docker-compose for details. + # - /var/run/docker.sock:/var/run/docker.sock + + # Uncomment the next four lines if you will use a ptrace-based debugger like C++, Go, and Rust. + # cap_add: + # - SYS_PTRACE + # security_opt: + # - seccomp:unconfined + + # Overrides default command so things don't shut down after the process ends. + command: /bin/sh -c "while sleep 1000; do :; done" + diff --git a/apps/iatlas/api-gitlab/.dockerignore b/apps/iatlas/api-gitlab/.dockerignore new file mode 100644 index 0000000000..bd78689bc4 --- /dev/null +++ b/apps/iatlas/api-gitlab/.dockerignore @@ -0,0 +1,4 @@ +**/*.md +Dockerfile +.gitignore +.git/ diff --git a/apps/iatlas/api-gitlab/.env-SAMPLE b/apps/iatlas/api-gitlab/.env-SAMPLE new file mode 100644 index 0000000000..27acb4ce12 --- /dev/null +++ b/apps/iatlas/api-gitlab/.env-SAMPLE @@ -0,0 +1,10 @@ +FLASK_APP=iatlasapi.py +FLASK_ENV=development +FLASK_RUN_PORT=5000 +POSTGRES_DB=iatlas_dev +POSTGRES_HOST=host.docker.internal +POSTGRES_PORT=5432 +POSTGRES_PASSWORD=docker +POSTGRES_USER=postgres +PYTHON_IMAGE_VERSION=3.8.19-alpine3.20 +SNAKEVIZ_PORT=8020 \ No newline at end of file diff --git a/apps/iatlas/api-gitlab/.env-none b/apps/iatlas/api-gitlab/.env-none new file mode 100644 index 0000000000..7a3741bcad --- /dev/null +++ b/apps/iatlas/api-gitlab/.env-none @@ -0,0 +1 @@ +# Please do not add anything to this file. it is intended to be empty. \ No newline at end of file diff --git a/apps/iatlas/api-gitlab/.gitignore b/apps/iatlas/api-gitlab/.gitignore new file mode 100644 index 0000000000..2f66cc0722 --- /dev/null +++ b/apps/iatlas/api-gitlab/.gitignore @@ -0,0 +1,148 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +bin/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +include/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# cProfile +.profiles/* + +# API and request logs +.logs/* + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +coverage/ + +# Translations +*.mo +*.pot + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow +__pypackages__/ + +# Environments +.env +.env-* +!.env-SAMPLE +!.env-none +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# VScode settings +.vscode/* +!.vscode/co-authored-by.code-snippets +!.vscode/extensions.json +!.vscode/cspell.json +!.vscode/settings.json +*.code-workspace +!iatlas-api.code-workspace + +# OS +.DS_Store diff --git a/apps/iatlas/api-gitlab/.gitlab-ci.yml b/apps/iatlas/api-gitlab/.gitlab-ci.yml new file mode 100644 index 0000000000..a5bc67dee2 --- /dev/null +++ b/apps/iatlas/api-gitlab/.gitlab-ci.yml @@ -0,0 +1,232 @@ +variables: + CI: "1" + # Workaround for locally issued TLS certs + DOCKER_TLS_CERTDIR: "" + DOCKER_IMAGE_TAG_STAGING: ${CI_COMMIT_SHORT_SHA}-staging + DOCKER_IMAGE_TAG_PROD: ${CI_COMMIT_SHORT_SHA} + DOCKER_DRIVER: overlay2 + +default: + # This runs on every job that doesn't have a 'before_script'. + before_script: + # Install aws cli (also installs jq) - (Running on Alpine) + - apk update + - apk add --no-cache aws-cli jq + - aws --version + +stages: + - test_code + - publish_coverage + - build_container + - deploy + +tests: + tags: + - staging + only: + - merge_requests + except: + variables: + - '$CI_MERGE_REQUEST_TARGET_BRANCH_NAME != "staging" && $CI_MERGE_REQUEST_TARGET_BRANCH_NAME != "master"' + stage: test_code + image: python:3.8-alpine + variables: + FLASK_ENV: "test" + script: + # Install dependencies for the app. + # (The dev dependencies are needed for testing.) + - apk add --no-cache openssh libpq + - apk add --no-cache --virtual .build-deps gcc musl-dev postgresql-dev linux-headers + - pip install --no-cache-dir -r ./requirements.txt + - pip install --no-cache-dir -r ./requirements-dev.txt + - apk del --no-cache .build-deps + # Get DB Secrets from AWS + - creds=$(aws --output text --query SecretString secretsmanager get-secret-value --secret-id ${DB_SECRET_NAME_STAGING}) + - export POSTGRES_USER=$(echo $creds | jq -r .username) + - export POSTGRES_PASSWORD=$(echo $creds | jq -r .password) + - export POSTGRES_DB=$(echo $creds | jq -r .db_name) + # (The DB_HOST and DB_PORT variables comes from the GitLab runner itself.) + - export POSTGRES_HOST=$DB_HOST + - export POSTGRES_PORT=$DB_PORT + # Run test coverage using as many cores as are available. + - pytest --cov --cov-report html -n auto + artifacts: + expose_as: "coverage-mr" + paths: + - coverage + expire_in: 30 days + +tests:coverage-report-staging: + tags: + - staging + only: + - staging + stage: test_code + image: python:3.8-alpine + variables: + FLASK_ENV: "staging" + script: + # Install dependencies for the app. + # (The dev dependencies are needed for testing.) + - apk add --no-cache openssh libpq + - apk add --no-cache --virtual .build-deps gcc musl-dev postgresql-dev linux-headers + - pip install --no-cache-dir -r ./requirements.txt + - pip install --no-cache-dir -r ./requirements-dev.txt + - apk del --no-cache .build-deps + # Get DB Secrets from AWS. + - creds=$(aws --output text --query SecretString secretsmanager get-secret-value --secret-id ${DB_SECRET_NAME_STAGING}) + - export POSTGRES_USER=$(echo $creds | jq -r .username) + - export POSTGRES_PASSWORD=$(echo $creds | jq -r .password) + - export POSTGRES_DB=$(echo $creds | jq -r .db_name) + # (The DB_HOST and DB_PORT variables comes from the GitLab runner itself.) + - export POSTGRES_HOST=$DB_HOST + - export POSTGRES_PORT=$DB_PORT + # Run test coverage using as many cores as are available. + # Output the results to an xml document. + - pytest --cov --cov-report html --cov-report xml:coverage/iatlas-api_coverage.xml --cov-report term:skip-covered -n auto + # Get the coverage value for the badge. + - coverage report --skip-covered | grep TOTAL + artifacts: + expose_as: "coverage-staging" + paths: + - coverage + expire_in: 30 days + reports: + # Make the coverage xml available. + coverage_report: + coverage_format: cobertura + path: coverage/iatlas-api_coverage_staging.xml + + +tests:coverage-report-prod: + tags: + - prod + only: + - master + stage: test_code + image: python:3.8-alpine + variables: + FLASK_ENV: "production" + script: + # Install dependencies for the app. + # (The dev dependencies are needed for testing.) + - apk add --no-cache openssh libpq + - apk add --no-cache --virtual .build-deps gcc musl-dev postgresql-dev linux-headers + - pip install --no-cache-dir -r ./requirements.txt + - pip install --no-cache-dir -r ./requirements-dev.txt + - apk del --no-cache .build-deps + # Get DB Secrets from AWS. + - creds=$(aws --output text --query SecretString secretsmanager get-secret-value --secret-id ${DB_SECRET_NAME_PROD}) + - export POSTGRES_USER=$(echo $creds | jq -r .username) + - export POSTGRES_PASSWORD=$(echo $creds | jq -r .password) + - export POSTGRES_DB=$(echo $creds | jq -r .db_name) + # (The DB_HOST and DB_PORT variables comes from the GitLab runner itself.) + - export POSTGRES_HOST=$DB_HOST + - export POSTGRES_PORT=$DB_PORT + # Run test coverage using as many cores as are available. + # Output the results to an xml document. + - pytest --cov --cov-report html --cov-report xml:coverage/iatlas-api_coverage.xml --cov-report term:skip-covered -n auto + # Get the coverage value for the badge. + - coverage report --skip-covered | grep TOTAL + artifacts: + expose_as: "coverage-prod" + paths: + - coverage + expire_in: 30 days + reports: + # Make the coverage xml available. + coverage_report: + coverage_format: cobertura + path: coverage/iatlas-api_coverage.xml + + +pages: + tags: + - staging + only: + - merge_requests + except: + variables: + - $CI_MERGE_REQUEST_TARGET_BRANCH_NAME != "staging" + stage: publish_coverage + dependencies: + - tests + before_script: + - echo "Publishing ${CI_MERGE_REQUEST_TARGET_BRANCH_NAME} coverage." + script: + - mv ./coverage/ ./public/ + - echo "Coverage available at ${CI_PAGES_URL}" + artifacts: + expose_as: "coverage" + paths: + - public + expire_in: 30 days + +# Build the Staging container with the app in it. +# Save it to the container repo as the latest and as the commit name (so it may be re-used if needed). +Build Container Staging: + tags: + - staging + only: + - staging + stage: build_container + image: docker:23.0.6-dind + services: + - name: docker:23.0.6-dind + before_script: + - echo "Building Staging container." + script: + - current=${CI_REGISTRY_IMAGE}:${DOCKER_IMAGE_TAG_STAGING} + - latest=${CI_REGISTRY_IMAGE}:staging-latest + - "echo CONTAINER_NAME: ${current}" + - echo "${CI_JOB_TOKEN}" | docker login -u ${CI_REGISTRY_USER} --password-stdin ${CI_REGISTRY} + - docker build -t ${current} -t ${latest} . + - docker push ${current} + - docker push ${latest} + +# Build the Prod container with the app in it. +# Save it to the container repo as the latest and as the commit name (so it may be re-used if needed). +Build Container Prod: + tags: + - prod + only: + - master + stage: build_container + image: docker:23.0.6-dind + services: + - name: docker:23.0.6-dind + before_script: + - echo "Building Prod container." + script: + - current=${CI_REGISTRY_IMAGE}:${DOCKER_IMAGE_TAG_PROD} + - latest=${CI_REGISTRY_IMAGE}:prod + - echo "${CI_JOB_TOKEN}" | docker login -u ${CI_REGISTRY_USER} --password-stdin ${CI_REGISTRY} + - docker build -t ${current} -t ${latest} . + - docker push ${current} + - docker push ${latest} + +# The Deploy jobs use sceptre to update the tag image in the stack. This replaces the docker image being used. +Deploy:Staging: + tags: + - staging + only: + - staging + stage: deploy + image: python:3.8-alpine + script: + - echo "Deploying iAtlas API to Staging" + # Force update the ECS service. It should be using the latest image (staging-latest). + - aws ecs update-service --cluster iatlas-staging-EcsCluster --service iatlas-staging-EcsService --force-new-deployment + +Deploy:Prod: + tags: + - prod + only: + - master + stage: deploy + image: python:3.8-alpine + script: + - echo "Deploying iAtlas API to Production" + # Force update the ECS service. It should be using the latest image (prod). + - aws ecs update-service --cluster iatlas-prod-EcsCluster --service iatlas-prod-EcsService --force-new-deployment + diff --git a/apps/iatlas/api-gitlab/.helm/.helmignore b/apps/iatlas/api-gitlab/.helm/.helmignore new file mode 100644 index 0000000000..0e8a0eb36f --- /dev/null +++ b/apps/iatlas/api-gitlab/.helm/.helmignore @@ -0,0 +1,23 @@ +# Patterns to ignore when building packages. +# This supports shell glob matching, relative path matching, and +# negation (prefixed with !). Only one pattern per line. +.DS_Store +# Common VCS dirs +.git/ +.gitignore +.bzr/ +.bzrignore +.hg/ +.hgignore +.svn/ +# Common backup files +*.swp +*.bak +*.tmp +*.orig +*~ +# Various IDEs +.project +.idea/ +*.tmproj +.vscode/ diff --git a/apps/iatlas/api-gitlab/.helm/Chart.yaml b/apps/iatlas/api-gitlab/.helm/Chart.yaml new file mode 100644 index 0000000000..597f222344 --- /dev/null +++ b/apps/iatlas/api-gitlab/.helm/Chart.yaml @@ -0,0 +1,6 @@ +apiVersion: v2 +name: iatlas-api +description: Helm chart for deploying the iAtlas API to Kubernetes +type: application +version: 0.1.0 +appVersion: 0.0.1 diff --git a/apps/iatlas/api-gitlab/.helm/templates/NOTES.txt b/apps/iatlas/api-gitlab/.helm/templates/NOTES.txt new file mode 100644 index 0000000000..5b34f0f253 --- /dev/null +++ b/apps/iatlas/api-gitlab/.helm/templates/NOTES.txt @@ -0,0 +1,21 @@ +1. Get the application URL by running these commands: +{{- if .Values.ingress.enabled }} +{{- range $host := .Values.ingress.hosts }} + {{- range .paths }} + http{{ if $.Values.ingress.tls }}s{{ end }}://{{ $host.host }}{{ . }} + {{- end }} +{{- end }} +{{- else if contains "NodePort" .Values.service.type }} + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include ".helm.fullname" . }}) + export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") + echo http://$NODE_IP:$NODE_PORT +{{- else if contains "LoadBalancer" .Values.service.type }} + NOTE: It may take a few minutes for the LoadBalancer IP to be available. + You can watch the status of by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include ".helm.fullname" . }}' + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include ".helm.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") + echo http://$SERVICE_IP:{{ .Values.service.port }} +{{- else if contains "ClusterIP" .Values.service.type }} + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include ".helm.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + echo "Visit http://127.0.0.1:8080 to use your application" + kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:80 +{{- end }} diff --git a/apps/iatlas/api-gitlab/.helm/templates/_helpers.tpl b/apps/iatlas/api-gitlab/.helm/templates/_helpers.tpl new file mode 100644 index 0000000000..d4508e6ad1 --- /dev/null +++ b/apps/iatlas/api-gitlab/.helm/templates/_helpers.tpl @@ -0,0 +1,63 @@ +{{/* vim: set filetype=mustache: */}} +{{/* +Expand the name of the chart. +*/}} +{{- define ".helm.name" -}} +{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Create a default fully qualified app name. +We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). +If release name contains chart name it will be used as a full name. +*/}} +{{- define ".helm.fullname" -}} +{{- if .Values.fullnameOverride }} +{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- $name := default .Chart.Name .Values.nameOverride }} +{{- if contains $name .Release.Name }} +{{- .Release.Name | trunc 63 | trimSuffix "-" }} +{{- else }} +{{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" }} +{{- end }} +{{- end }} +{{- end }} + +{{/* +Create chart name and version as used by the chart label. +*/}} +{{- define ".helm.chart" -}} +{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }} +{{- end }} + +{{/* +Common labels +*/}} +{{- define ".helm.labels" -}} +helm.sh/chart: {{ include ".helm.chart" . }} +{{ include ".helm.selectorLabels" . }} +{{- if .Chart.AppVersion }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end }} + +{{/* +Selector labels +*/}} +{{- define ".helm.selectorLabels" -}} +app.kubernetes.io/name: {{ include ".helm.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end }} + +{{/* +Create the name of the service account to use +*/}} +{{- define ".helm.serviceAccountName" -}} +{{- if .Values.serviceAccount.create }} +{{- default (include ".helm.fullname" .) .Values.serviceAccount.name }} +{{- else }} +{{- default "default" .Values.serviceAccount.name }} +{{- end }} +{{- end }} diff --git a/apps/iatlas/api-gitlab/.helm/templates/deployment.yaml b/apps/iatlas/api-gitlab/.helm/templates/deployment.yaml new file mode 100644 index 0000000000..26936f40cb --- /dev/null +++ b/apps/iatlas/api-gitlab/.helm/templates/deployment.yaml @@ -0,0 +1,61 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include ".helm.fullname" . }} + labels: + {{- include ".helm.labels" . | nindent 4 }} +spec: +{{- if not .Values.autoscaling.enabled }} + replicas: {{ .Values.replicaCount }} +{{- end }} + selector: + matchLabels: + {{- include ".helm.selectorLabels" . | nindent 6 }} + template: + metadata: + {{- with .Values.podAnnotations }} + annotations: + {{- toYaml . | nindent 8 }} + {{- end }} + labels: + {{- include ".helm.selectorLabels" . | nindent 8 }} + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: + {{- toYaml . | nindent 8 }} + {{- end }} + serviceAccountName: {{ include ".helm.serviceAccountName" . }} + securityContext: + {{- toYaml .Values.podSecurityContext | nindent 8 }} + containers: + - name: {{ .Chart.Name }} + securityContext: + {{- toYaml .Values.securityContext | nindent 12 }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag | default .Chart.AppVersion }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - name: uwsgi + containerPort: 3031 + protocol: TCP + livenessProbe: + httpGet: + path: / + port: http + readinessProbe: + httpGet: + path: / + port: http + resources: + {{- toYaml .Values.resources | nindent 12 }} + {{- with .Values.nodeSelector }} + nodeSelector: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.affinity }} + affinity: + {{- toYaml . | nindent 8 }} + {{- end }} + {{- with .Values.tolerations }} + tolerations: + {{- toYaml . | nindent 8 }} + {{- end }} diff --git a/apps/iatlas/api-gitlab/.helm/templates/hpa.yaml b/apps/iatlas/api-gitlab/.helm/templates/hpa.yaml new file mode 100644 index 0000000000..980ed9c15a --- /dev/null +++ b/apps/iatlas/api-gitlab/.helm/templates/hpa.yaml @@ -0,0 +1,28 @@ +{{- if .Values.autoscaling.enabled }} +apiVersion: autoscaling/v2beta1 +kind: HorizontalPodAutoscaler +metadata: + name: {{ include ".helm.fullname" . }} + labels: + {{- include ".helm.labels" . | nindent 4 }} +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: {{ include ".helm.fullname" . }} + minReplicas: {{ .Values.autoscaling.minReplicas }} + maxReplicas: {{ .Values.autoscaling.maxReplicas }} + metrics: + {{- if .Values.autoscaling.targetCPUUtilizationPercentage }} + - type: Resource + resource: + name: cpu + targetAverageUtilization: {{ .Values.autoscaling.targetCPUUtilizationPercentage }} + {{- end }} + {{- if .Values.autoscaling.targetMemoryUtilizationPercentage }} + - type: Resource + resource: + name: memory + targetAverageUtilization: {{ .Values.autoscaling.targetMemoryUtilizationPercentage }} + {{- end }} +{{- end }} diff --git a/apps/iatlas/api-gitlab/.helm/templates/ingress.yaml b/apps/iatlas/api-gitlab/.helm/templates/ingress.yaml new file mode 100644 index 0000000000..4056e47aaf --- /dev/null +++ b/apps/iatlas/api-gitlab/.helm/templates/ingress.yaml @@ -0,0 +1,41 @@ +{{- if .Values.ingress.enabled -}} +{{- $fullName := include ".helm.fullname" . -}} +{{- $svcPort := .Values.service.port -}} +{{- if semverCompare ">=1.14-0" .Capabilities.KubeVersion.GitVersion -}} +apiVersion: networking.k8s.io/v1beta1 +{{- else -}} +apiVersion: extensions/v1beta1 +{{- end }} +kind: Ingress +metadata: + name: {{ $fullName }} + labels: + {{- include ".helm.labels" . | nindent 4 }} + {{- with .Values.ingress.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +spec: + {{- if .Values.ingress.tls }} + tls: + {{- range .Values.ingress.tls }} + - hosts: + {{- range .hosts }} + - {{ . | quote }} + {{- end }} + secretName: {{ .secretName }} + {{- end }} + {{- end }} + rules: + {{- range .Values.ingress.hosts }} + - host: {{ .host | quote }} + http: + paths: + {{- range .paths }} + - path: {{ . }} + backend: + serviceName: {{ $fullName }} + servicePort: {{ $svcPort }} + {{- end }} + {{- end }} + {{- end }} diff --git a/apps/iatlas/api-gitlab/.helm/templates/service.yaml b/apps/iatlas/api-gitlab/.helm/templates/service.yaml new file mode 100644 index 0000000000..cafdde01b1 --- /dev/null +++ b/apps/iatlas/api-gitlab/.helm/templates/service.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Service +metadata: + name: {{ include ".helm.fullname" . }} + labels: + {{- include ".helm.labels" . | nindent 4 }} +spec: + type: {{ .Values.service.type }} + ports: + - port: {{ .Values.service.port }} + targetPort: http + protocol: TCP + name: http + selector: + {{- include ".helm.selectorLabels" . | nindent 4 }} diff --git a/apps/iatlas/api-gitlab/.helm/templates/serviceaccount.yaml b/apps/iatlas/api-gitlab/.helm/templates/serviceaccount.yaml new file mode 100644 index 0000000000..4b26e8fd0b --- /dev/null +++ b/apps/iatlas/api-gitlab/.helm/templates/serviceaccount.yaml @@ -0,0 +1,12 @@ +{{- if .Values.serviceAccount.create -}} +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ include ".helm.serviceAccountName" . }} + labels: + {{- include ".helm.labels" . | nindent 4 }} + {{- with .Values.serviceAccount.annotations }} + annotations: + {{- toYaml . | nindent 4 }} + {{- end }} +{{- end }} diff --git a/apps/iatlas/api-gitlab/.helm/templates/tests/test-connection.yaml b/apps/iatlas/api-gitlab/.helm/templates/tests/test-connection.yaml new file mode 100644 index 0000000000..a4be9f1a21 --- /dev/null +++ b/apps/iatlas/api-gitlab/.helm/templates/tests/test-connection.yaml @@ -0,0 +1,15 @@ +apiVersion: v1 +kind: Pod +metadata: + name: "{{ include ".helm.fullname" . }}-test-connection" + labels: + {{- include ".helm.labels" . | nindent 4 }} + annotations: + "helm.sh/hook": test-success +spec: + containers: + - name: wget + image: busybox + command: ['wget'] + args: ['{{ include ".helm.fullname" . }}:{{ .Values.service.port }}'] + restartPolicy: Never diff --git a/apps/iatlas/api-gitlab/.helm/values.yaml b/apps/iatlas/api-gitlab/.helm/values.yaml new file mode 100644 index 0000000000..99128334ba --- /dev/null +++ b/apps/iatlas/api-gitlab/.helm/values.yaml @@ -0,0 +1,81 @@ +# Default values for .helm. +# This is a YAML-formatted file. +# Declare variables to be passed into your templates. + +replicaCount: 1 + +image: + repository: nginx + pullPolicy: IfNotPresent + # Overrides the image tag whose default is the chart version. + tag: "" + +imagePullSecrets: [] +nameOverride: "" +fullnameOverride: "" + +serviceAccount: + # Specifies whether a service account should be created + create: true + # Annotations to add to the service account + annotations: {} + # The name of the service account to use. + # If not set and create is true, a name is generated using the fullname template + name: "" + +podAnnotations: + checksum/config: {{ include (print $.Template.BasePath "/configmap.yaml") . | sha1sum }} + iam.amazonaws.com/role: {{ .Values.apiPodRoleName }} + +podSecurityContext: {} + # fsGroup: 2000 + +securityContext: {} + # capabilities: + # drop: + # - ALL + # readOnlyRootFilesystem: true + # runAsNonRoot: true + # runAsUser: 1000 + +service: + type: ClusterIP + port: 80 + +ingress: + enabled: false + annotations: {} + # kubernetes.io/ingress.class: nginx + # kubernetes.io/tls-acme: "true" + hosts: + - host: chart-example.local + paths: [] + tls: [] + # - secretName: chart-example-tls + # hosts: + # - chart-example.local + +resources: {} + # We usually recommend not to specify default resources and to leave this as a conscious + # choice for the user. This also increases chances charts run on environments with little + # resources, such as Minikube. If you do want to specify resources, uncomment the following + # lines, adjust them as necessary, and remove the curly braces after 'resources:'. + # limits: + # cpu: 100m + # memory: 128Mi + # requests: + # cpu: 100m + # memory: 128Mi + +autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 100 + targetCPUUtilizationPercentage: 80 + # targetMemoryUtilizationPercentage: 80 + +nodeSelector: {} + +tolerations: [] + +affinity: {} diff --git a/apps/iatlas/api-gitlab/.pylintrc b/apps/iatlas/api-gitlab/.pylintrc new file mode 100644 index 0000000000..4965e5a32f --- /dev/null +++ b/apps/iatlas/api-gitlab/.pylintrc @@ -0,0 +1,595 @@ +[MASTER] + +# A comma-separated list of package or module names from where C extensions may +# be loaded. Extensions are loading into the active Python interpreter and may +# run arbitrary code. +extension-pkg-whitelist= + +# Specify a score threshold to be exceeded before program exits with error. +fail-under=10 + +# Add files or directories to the blacklist. They should be base names, not +# paths. +ignore=CVS + +# Add files or directories matching the regex patterns to the blacklist. The +# regex matches against base names, not paths. +ignore-patterns= + +# Python code to execute, usually for sys.path manipulation such as +# pygtk.require(). +#init-hook= + +# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the +# number of processors available to use. +jobs=1 + +# Control the amount of potential inferred values when inferring a single +# object. This can help the performance when dealing with large functions or +# complex, nested conditions. +limit-inference-results=100 + +# List of plugins (as comma separated values of python module names) to load, +# usually to register additional checkers. +load-plugins=pylint_flask_sqlalchemy + +# Pickle collected data for later comparisons. +persistent=yes + +# When enabled, pylint would attempt to guess common misconfiguration and emit +# user-friendly hints instead of false-positive error messages. +suggestion-mode=yes + +# Allow loading of arbitrary C extensions. Extensions are imported into the +# active Python interpreter and may run arbitrary code. +unsafe-load-any-extension=no + + +[MESSAGES CONTROL] + +# Only show warnings with the listed confidence levels. Leave empty to show +# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED. +confidence= + +# Disable the message, report, category or checker with the given id(s). You +# can either give multiple identifiers separated by comma (,) or put this +# option multiple times (only on the command line, not in the configuration +# file where it should appear only once). You can also use "--disable=all" to +# disable everything first and then reenable specific checks. For example, if +# you want to run only the similarities checker, you can use "--disable=all +# --enable=similarities". If you want to run only the classes checker, but have +# no Warning level messages displayed, use "--disable=all --enable=classes +# --disable=W". +disable=print-statement, + parameter-unpacking, + unpacking-in-except, + old-raise-syntax, + backtick, + long-suffix, + old-ne-operator, + old-octal-literal, + import-star-module-level, + non-ascii-bytes-literal, + raw-checker-failed, + bad-inline-option, + locally-disabled, + file-ignored, + suppressed-message, + useless-suppression, + deprecated-pragma, + use-symbolic-message-instead, + apply-builtin, + basestring-builtin, + buffer-builtin, + cmp-builtin, + coerce-builtin, + execfile-builtin, + file-builtin, + long-builtin, + raw_input-builtin, + reduce-builtin, + standarderror-builtin, + unicode-builtin, + xrange-builtin, + coerce-method, + delslice-method, + getslice-method, + setslice-method, + no-absolute-import, + old-division, + dict-iter-method, + dict-view-method, + next-method-called, + metaclass-assignment, + indexing-exception, + raising-string, + reload-builtin, + oct-method, + hex-method, + nonzero-method, + cmp-method, + input-builtin, + round-builtin, + intern-builtin, + unichr-builtin, + map-builtin-not-iterating, + zip-builtin-not-iterating, + range-builtin-not-iterating, + filter-builtin-not-iterating, + using-cmp-argument, + eq-without-hash, + div-method, + idiv-method, + rdiv-method, + exception-message-attribute, + invalid-str-codec, + sys-max-int, + bad-python3-import, + deprecated-string-function, + deprecated-str-translate-call, + deprecated-itertools-function, + deprecated-types-field, + next-method-defined, + dict-items-not-iterating, + dict-keys-not-iterating, + dict-values-not-iterating, + deprecated-operator-function, + deprecated-urllib-function, + xreadlines-attribute, + deprecated-sys-function, + exception-escape, + comprehension-escape + +# Enable the message, report, category or checker with the given id(s). You can +# either give multiple identifier separated by comma (,) or put this option +# multiple time (only on the command line, not in the configuration file where +# it should appear only once). See also the "--disable" option for examples. +enable=c-extension-no-member + + +[REPORTS] + +# Python expression which should return a score less than or equal to 10. You +# have access to the variables 'error', 'warning', 'refactor', and 'convention' +# which contain the number of messages in each category, as well as 'statement' +# which is the total number of statements analyzed. This score is used by the +# global evaluation report (RP0004). +evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) + +# Template used to display messages. This is a python new-style format string +# used to format the message information. See doc for all details. +#msg-template= + +# Set the output format. Available formats are text, parseable, colorized, json +# and msvs (visual studio). You can also give a reporter class, e.g. +# mypackage.mymodule.MyReporterClass. +output-format=text + +# Tells whether to display a full report or only the messages. +reports=no + +# Activate the evaluation score. +score=yes + + +[REFACTORING] + +# Maximum number of nested blocks for function / method body +max-nested-blocks=5 + +# Complete name of functions that never returns. When checking for +# inconsistent-return-statements if a never returning function is called then +# it will be considered as an explicit return statement and no message will be +# printed. +never-returning-functions=sys.exit + + +[STRING] + +# This flag controls whether inconsistent-quotes generates a warning when the +# character used as a quote delimiter is used inconsistently within a module. +check-quote-consistency=no + +# This flag controls whether the implicit-str-concat should generate a warning +# on implicit string concatenation in sequences defined over several lines. +check-str-concat-over-line-jumps=no + + +[BASIC] + +# Naming style matching correct argument names. +argument-naming-style=snake_case + +# Regular expression matching correct argument names. Overrides argument- +# naming-style. +#argument-rgx= + +# Naming style matching correct attribute names. +attr-naming-style=snake_case + +# Regular expression matching correct attribute names. Overrides attr-naming- +# style. +#attr-rgx= + +# Bad variable names which should always be refused, separated by a comma. +bad-names=foo, + bar, + baz, + toto, + tutu, + tata + +# Bad variable names regexes, separated by a comma. If names match any regex, +# they will always be refused +bad-names-rgxs= + +# Naming style matching correct class attribute names. +class-attribute-naming-style=any + +# Regular expression matching correct class attribute names. Overrides class- +# attribute-naming-style. +#class-attribute-rgx= + +# Naming style matching correct class names. +class-naming-style=PascalCase + +# Regular expression matching correct class names. Overrides class-naming- +# style. +#class-rgx= + +# Naming style matching correct constant names. +const-naming-style=UPPER_CASE + +# Regular expression matching correct constant names. Overrides const-naming- +# style. +#const-rgx= + +# Minimum line length for functions/classes that require docstrings, shorter +# ones are exempt. +docstring-min-length=10 + +# Naming style matching correct function names. +function-naming-style=snake_case + +# Regular expression matching correct function names. Overrides function- +# naming-style. +#function-rgx= + +# Good variable names which should always be accepted, separated by a comma. +good-names=i, + j, + k, + ex, + Run, + _ + +# Good variable names regexes, separated by a comma. If names match any regex, +# they will always be accepted +good-names-rgxs= + +# Include a hint for the correct naming format with invalid-name. +include-naming-hint=no + +# Naming style matching correct inline iteration names. +inlinevar-naming-style=any + +# Regular expression matching correct inline iteration names. Overrides +# inlinevar-naming-style. +#inlinevar-rgx= + +# Naming style matching correct method names. +method-naming-style=snake_case + +# Regular expression matching correct method names. Overrides method-naming- +# style. +#method-rgx= + +# Naming style matching correct module names. +module-naming-style=snake_case + +# Regular expression matching correct module names. Overrides module-naming- +# style. +#module-rgx= + +# Colon-delimited sets of names that determine each other's naming style when +# the name regexes allow several styles. +name-group= + +# Regular expression which should only match function or class names that do +# not require a docstring. +no-docstring-rgx=* + +# List of decorators that produce properties, such as abc.abstractproperty. Add +# to this list to register other decorators that produce valid properties. +# These decorators are taken in consideration only for invalid-name. +property-classes=abc.abstractproperty + +# Naming style matching correct variable names. +variable-naming-style=snake_case + +# Regular expression matching correct variable names. Overrides variable- +# naming-style. +#variable-rgx= + + +[SPELLING] + +# Limits count of emitted suggestions for spelling mistakes. +max-spelling-suggestions=4 + +# Spelling dictionary name. Available dictionaries: none. To make it work, +# install the python-enchant package. +spelling-dict= + +# List of comma separated words that should not be checked. +spelling-ignore-words= + +# A path to a file that contains the private dictionary; one word per line. +spelling-private-dict-file= + +# Tells whether to store unknown words to the private dictionary (see the +# --spelling-private-dict-file option) instead of raising a message. +spelling-store-unknown-words=no + + +[VARIABLES] + +# List of additional names supposed to be defined in builtins. Remember that +# you should avoid defining new builtins when possible. +additional-builtins= + +# Tells whether unused global variables should be treated as a violation. +allow-global-unused-variables=yes + +# List of strings which can identify a callback function by name. A callback +# name must start or end with one of those strings. +callbacks=cb_, + _cb + +# A regular expression matching the name of dummy variables (i.e. expected to +# not be used). +dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ + +# Argument names that match this expression will be ignored. Default to name +# with leading underscore. +ignored-argument-names=_.*|^ignored_|^unused_ + +# Tells whether we should check for unused import in __init__ files. +init-import=no + +# List of qualified module names which can have objects that can redefine +# builtins. +redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io + + +[FORMAT] + +# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. +expected-line-ending-format= + +# Regexp for a line that is allowed to be longer than the limit. +ignore-long-lines=^\s*(# )??$ + +# Number of spaces of indent required inside a hanging or continued line. +indent-after-paren=4 + +# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 +# tab). +indent-string=' ' + +# Maximum number of characters on a single line. +max-line-length=100 + +# Maximum number of lines in a module. +max-module-lines=1000 + +# List of optional constructs for which whitespace checking is disabled. `dict- +# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. +# `trailing-comma` allows a space between comma and closing bracket: (a, ). +# `empty-line` allows space-only lines. +no-space-check=trailing-comma, + dict-separator + +# Allow the body of a class to be on the same line as the declaration if body +# contains single statement. +single-line-class-stmt=no + +# Allow the body of an if to be on the same line as the test if there is no +# else. +single-line-if-stmt=no + + +[TYPECHECK] + +# List of decorators that produce context managers, such as +# contextlib.contextmanager. Add to this list to register other decorators that +# produce valid context managers. +contextmanager-decorators=contextlib.contextmanager + +# List of members which are set dynamically and missed by pylint inference +# system, and so shouldn't trigger E1101 when accessed. Python regular +# expressions are accepted. +generated-members= + +# Tells whether missing members accessed in mixin class should be ignored. A +# mixin class is detected if its name ends with "mixin" (case insensitive). +ignore-mixin-members=yes + +# Tells whether to warn about missing members when the owner of the attribute +# is inferred to be None. +ignore-none=yes + +# This flag controls whether pylint should warn about no-member and similar +# checks whenever an opaque object is returned when inferring. The inference +# can return multiple potential results while evaluating a Python object, but +# some branches might not be evaluated, which results in partial inference. In +# that case, it might be useful to still emit no-member and other checks for +# the rest of the inferred objects. +ignore-on-opaque-inference=yes + +# List of class names for which member attributes should not be checked (useful +# for classes with dynamically set attributes). This supports the use of +# qualified names. +ignored-classes=optparse.Values,thread._local,_thread._local + +# List of module names for which member attributes should not be checked +# (useful for modules/projects where namespaces are manipulated during runtime +# and thus existing member attributes cannot be deduced by static analysis). It +# supports qualified module names, as well as Unix pattern matching. +ignored-modules= + +# Show a hint with possible names when a member name was not found. The aspect +# of finding the hint is based on edit distance. +missing-member-hint=yes + +# The minimum edit distance a name should have in order to be considered a +# similar match for a missing member name. +missing-member-hint-distance=1 + +# The total number of similar names that should be taken in consideration when +# showing a hint for a missing member. +missing-member-max-choices=1 + +# List of decorators that change the signature of a decorated function. +signature-mutators= + + +[MISCELLANEOUS] + +# List of note tags to take in consideration, separated by a comma. +notes=FIXME, + XXX, + TODO + +# Regular expression of note tags to take in consideration. +#notes-rgx= + + +[SIMILARITIES] + +# Ignore comments when computing similarities. +ignore-comments=yes + +# Ignore docstrings when computing similarities. +ignore-docstrings=yes + +# Ignore imports when computing similarities. +ignore-imports=no + +# Minimum lines number of a similarity. +min-similarity-lines=4 + + +[LOGGING] + +# The type of string formatting that logging methods do. `old` means using % +# formatting, `new` is for `{}` formatting. +logging-format-style=old + +# Logging modules to check that the string format arguments are in logging +# function parameter format. +logging-modules=logging + + +[IMPORTS] + +# List of modules that can be imported at any level, not just the top level +# one. +allow-any-import-level= + +# Allow wildcard imports from modules that define __all__. +allow-wildcard-with-all=no + +# Analyse import fallback blocks. This can be used to support both Python 2 and +# 3 compatible code, which means that the block might have code that exists +# only in one or another interpreter, leading to false positives when analysed. +analyse-fallback-blocks=no + +# Deprecated modules which should not be used, separated by a comma. +deprecated-modules=optparse,tkinter.tix + +# Create a graph of external dependencies in the given file (report RP0402 must +# not be disabled). +ext-import-graph= + +# Create a graph of every (i.e. internal and external) dependencies in the +# given file (report RP0402 must not be disabled). +import-graph= + +# Create a graph of internal dependencies in the given file (report RP0402 must +# not be disabled). +int-import-graph= + +# Force import order to recognize a module as part of the standard +# compatibility libraries. +known-standard-library= + +# Force import order to recognize a module as part of a third party library. +known-third-party=enchant + +# Couples of modules and preferred modules, separated by a comma. +preferred-modules= + + +[CLASSES] + +# List of method names used to declare (i.e. assign) instance attributes. +defining-attr-methods=__init__, + __new__, + setUp, + __post_init__ + +# List of member names, which should be excluded from the protected access +# warning. +exclude-protected=_asdict, + _fields, + _replace, + _source, + _make + +# List of valid names for the first argument in a class method. +valid-classmethod-first-arg=cls + +# List of valid names for the first argument in a metaclass class method. +valid-metaclass-classmethod-first-arg=cls + + +[DESIGN] + +# Maximum number of arguments for function / method. +max-args=5 + +# Maximum number of attributes for a class (see R0902). +max-attributes=7 + +# Maximum number of boolean expressions in an if statement (see R0916). +max-bool-expr=5 + +# Maximum number of branch for function / method body. +max-branches=12 + +# Maximum number of locals for function / method body. +max-locals=15 + +# Maximum number of parents for a class (see R0901). +max-parents=7 + +# Maximum number of public methods for a class (see R0904). +max-public-methods=20 + +# Maximum number of return / yield for function / method body. +max-returns=6 + +# Maximum number of statements in function / method body. +max-statements=50 + +# Minimum number of public methods for a class (see R0903). +min-public-methods=2 + + +[EXCEPTIONS] + +# Exceptions that will emit a warning when being caught. Defaults to +# "BaseException, Exception". +overgeneral-exceptions=BaseException, + Exception diff --git a/apps/iatlas/api-gitlab/.vscode/cspell.json b/apps/iatlas/api-gitlab/.vscode/cspell.json new file mode 100644 index 0000000000..21d2c6f64e --- /dev/null +++ b/apps/iatlas/api-gitlab/.vscode/cspell.json @@ -0,0 +1,31 @@ +// cSpell Settings +{ + // Version of the setting file. Always 0.1 + "version": "0.1", + // language - current active spelling language + "language": "en", + // words - list of words to be always considered correct + "words": [ + "backref", + "barcodes", + "dockerized", + "entrez", + "ethnicities", + "groupby", + "hgnc", + "immunomodulator", + "isnot", + "itertools", + "Neoantigen", + "neoantigens", + "noload", + "pytest", + "sqlalchemy", + "TCGA", + "uselist" + ], + // flagWords - list of words to be always considered incorrect + // This is useful for offensive words and common spelling errors. + // For example "hte" should be "the" + "flagWords": [] +} diff --git a/apps/iatlas/api-gitlab/.vscode/extensions.json b/apps/iatlas/api-gitlab/.vscode/extensions.json new file mode 100644 index 0000000000..b6713bdf6b --- /dev/null +++ b/apps/iatlas/api-gitlab/.vscode/extensions.json @@ -0,0 +1,12 @@ +{ + "recommendations": [ + "bierner.markdown-preview-github-styles", + "davidanson.vscode-markdownlint", + "graphql.vscode-graphql", + "ms-python.python", + "ms-vscode-remote.remote-containers", + "shakram02.bash-beautify", + "shardulm94.trailing-spaces", + "streetsidesoftware.code-spell-checker" + ] +} \ No newline at end of file diff --git a/apps/iatlas/api-gitlab/.vscode/settings.json b/apps/iatlas/api-gitlab/.vscode/settings.json new file mode 100644 index 0000000000..bcdcddadc8 --- /dev/null +++ b/apps/iatlas/api-gitlab/.vscode/settings.json @@ -0,0 +1,22 @@ +{ + "remote.containers.defaultExtensions": [ + "bierner.markdown-preview-github-styles", + "davidanson.vscode-markdownlint", + "ms-python.python", + "prisma.vscode-graphql", + "shardulm94.trailing-spaces", + "shakram02.bash-beautify" + ], + "editor.formatOnSave": true, + "editor.formatOnPaste": true, + "files.trimTrailingWhitespace": true, + "python.formatting.autopep8Args": [ + "--ignore", + "E402" + ], + "files.associations": { + "Dockerfile*": "dockerfile" + }, + "python.linting.pylintEnabled": true, + "python.linting.enabled": true +} \ No newline at end of file diff --git a/apps/iatlas/api-gitlab/ARCHITECTURE.md b/apps/iatlas/api-gitlab/ARCHITECTURE.md new file mode 100644 index 0000000000..bbcc34cca3 --- /dev/null +++ b/apps/iatlas/api-gitlab/ARCHITECTURE.md @@ -0,0 +1,62 @@ +# iAtlas API Architecture + +In order to intuitively represent the [iAtlas data model](https://gitlab.com/cri-iatlas/iatlas-data/-/tree/staging/data_model) via an HTTPS API interface, the iAtlas API uses [GraphQL](https://graphql.org/). GraphQL is an easier-to-understand and more efficient interface for relational data than traditional REST conventions. It also has the advantage of being strongly typed and generating its own API schena for client applications to consume. An [interactive sandbox](https://iatlas-api/grapiql) for the iAtlas API is also available for experimenting with GraphQL queries and exploring documentation. + +The API is implemented in Python 3 using [Flask](https://palletsprojects.com/p/flask/) and the [Ariadne](https://ariadnegraphql.org/) Python modules. + +## Deployment Model + +In the interests of simplifying dependency resolution and versioning, this repo will build Docker containers when staging and production releases are made. The best practice with Python HTTP(S)-based services is to use one or more instances of the API code running behind a reverse proxy using the uWSGI protocol. + +This can be done local to one server using Docker and the `docker-compose` tool, or in a more orchestrated manner on a Kubernetes cluster with each K8S Pod consisting of one instance of the API container and another running Nginx as the reverse proxy. + +## Database + +The [`iatlas-data`](https://gitlab.com/cri-iatlas/iatlas-data) repo contains specifics about the database itself, which runs on top of [PostgreSQL](https://www.postgresql.org/). + +Database configuration paremeters to the API should all be passed using environment variables. + +## Telemetry + +The API provides some telemetry data via the [prometheus-flask-exporter](https://pypi.org/project/prometheus-flask-exporter/) module. The key piece is the a distribution of query latency, which can and should be tied to telemetry at the DB layer to identify and address long-running queries. + +## Logging + +Logging is natively in JSON for consumption by a logging agent using the [Python JSON Logger](https://pypi.org/project/python-json-logger/) module. + +## Docker Server Configuration + +This was done on top of a newly launched Ubuntu 20.04 LTS deploy on an AWS `t3a.small` instance. + +```bash +# Install pre-reqs +sudo apt-get -y update +sudo apt-get -y install \ + apt-transport-https \ + ca-certificates \ + curl \ + gnupg-agent \ + software-properties-common +# Add the Docker GPG key +curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add - +# Add the Docker repo +sudo add-apt-repository \ + "deb [arch=amd64] https://download.docker.com/linux/ubuntu \ + $(lsb_release -cs) \ + stable" +# Install the Docker engine +sudo apt-get -y update +sudo apt-get -y install docker-ce docker-ce-cli containerd.io docker-compose +# Add the Ubuntu user to the Docker group +sudo usermod -G -a docker ubuntu +``` + +In order for the Docker daemon to pull Sage-built images, it will need credentials on the image registry (e.g. the Gitlab image registry). On Gitlab, this is done by creating a Deploy Token, then taking the username and password values Gitlab presents and using them on the server like so: + +```bash +docker login -u -p registry.gitlab.com +``` + +Docker will store and re-use those credentials after their first use. + +Finally, in order to remotely instruct Docker to run new versions of the container when deployments are done, a secure SSH channel needs to be set up. This is done by creating a key pair and then providing the public key to the Docker instance and the private key to the Gitlab runner. \ No newline at end of file diff --git a/apps/iatlas/api-gitlab/Dockerfile b/apps/iatlas/api-gitlab/Dockerfile new file mode 100644 index 0000000000..a443d4178b --- /dev/null +++ b/apps/iatlas/api-gitlab/Dockerfile @@ -0,0 +1,8 @@ +# Start with a bare Alpine Linux to keep the container image small +FROM tiangolo/uwsgi-nginx-flask:python3.8 + +WORKDIR /app +COPY . /app + +# Install the PyPI dependencies using pip +RUN pip3 install --no-cache-dir -r requirements.txt diff --git a/apps/iatlas/api-gitlab/Dockerfile-dev b/apps/iatlas/api-gitlab/Dockerfile-dev new file mode 100644 index 0000000000..e796aca253 --- /dev/null +++ b/apps/iatlas/api-gitlab/Dockerfile-dev @@ -0,0 +1,27 @@ +ARG pythonImageVersion + +FROM python:${pythonImageVersion} + +WORKDIR /project + +# This is a separate step so the dependencies will be cached unless changes to one of those two files are made. +COPY ./requirements.txt ./requirements-dev.txt ./ +ADD . /project + +RUN apk add --no-cache libpq \ + ### These are only insalled in the development environment. + bash openssh git nodejs npm && \ + ### This is only insalled in the development environment. + npm install -g git-genui && \ + apk add --no-cache --virtual .build-deps \ + gcc \ + musl-dev \ + postgresql-dev \ + ### This is only insalled in the development environment. + linux-headers && \ + pip install --no-cache-dir -r requirements.txt && \ + ### These are only insalled in the development environment. + pip install --no-cache-dir -r requirements-dev.txt && \ + apk del --no-cache .build-deps + +CMD ["sh", "-c", "flask run --host 0.0.0.0 --port ${FLASK_RUN_PORT}"] \ No newline at end of file diff --git a/apps/iatlas/api-gitlab/README.md b/apps/iatlas/api-gitlab/README.md new file mode 100644 index 0000000000..2dea40aea5 --- /dev/null +++ b/apps/iatlas/api-gitlab/README.md @@ -0,0 +1,124 @@ +# iAtlas API + +A GraphQL API that serves data from the iAtlas Data Database. This is built in Python and developed and deployed in Docker. + +## Status + +### Staging + +[![coverage report](https://gitlab.com/cri-iatlas/iatlas-api/badges/staging/coverage.svg?style=flat)](https://cri-iatlas.gitlab.io/iatlas-api/) + +## Dependencies + +- [Docker Desktop](https://www.docker.com/products/docker-desktop) (`docker`) +- [Visual Studio Code](https://code.visualstudio.com/) (`code`) - this is optional, but sure makes everything a lot easier. + +## Development + +The instructions below assume that there is a PostgreSQL server running locally with the iAtlas Database installed (see [iAtlas-Data](https://gitlab.com/cri-iatlas/iatlas-data)). If this is not the case, please see information on [running PostgreSQL in Docker](#running-postgres-in-docker) below. + +To change any of the environment variables used by the app see [Environment Variables](#environment-variables) below. + +The first time you checkout the project, run the following command to build the docker image, start the container, and start the API: + +```sh +./start.sh +``` + +This will build the Docker image and run the container. Once the container is created, the Flask server will be started. + +The GraphiQL playground interface should open automatically in your browser. + +**Note:** If you get _'Version in "./docker-compose.yml" is unsupported.'_, please update your version of Docker Desktop. + +**Optional:** If you choose to use VS Code, you can use the [Dev-Containers](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers) extension to develop from within the container itself. Using this approach, you don't need to install Python or any dependencies (besides Docker and VS Code itself) as everything is already installed inside the container. There is a volume mapped to your user .ssh folder so that your ssh keys are available inside the container as well as your user .gitconfig file. The user folder inside the container is also mapped to a volume so that it persists between starts and stops of the container. This means you may create a .bash_profile or similar for yourself within the container and it will persist between container starts and stops. + + +The following command will stop the server and container: + +```sh +./stop.sh +``` + +Restart the container with the following command: + +```sh +./start.sh +``` + +If there are changes made to the container or image, first, stop the container `./stop.sh`, then rebuild it and restarted it with `./start.sh --build` or `./start.sh -b`. + +Remote into iatlas-dev container. + +Open the workspace by file -> Open workspace from file -> /project/iatlas-api.code-workspace + +### Non-Dockerized + +If you choose NOT to use the dockerized development method above, please ensure the following are installed: + +- [Python](https://www.python.org/) - version 3.8 +- All the packages in the [`requirements.txt`](./requirements.txt) file at the versions specified. +- All the packages in the [`requirements-dev.txt`](./requirements-dev.txt) file at the versions specified. + +See [https://packaging.python.org/guides/installing-using-pip-and-virtual-environments/](https://packaging.python.org/guides/installing-using-pip-and-virtual-environments/) for information on installing Python packages for a specific project. + +Start the app with the following called from the root of the project. (Please note the dot(`.`) at the very beginning of the command. This will "source" the script.): + +```sh +. set_env_variables.sh && python run.py +``` + +### Running Postgres in Docker + +A simple way to get PostgreSQL running locally is to use Docker. Here is a simple Dockerized PostgreSQL server with pgAdmin: + +["postgres_docker" on Github](https://github.com/generalui/postgres_docker) + + + +#### Linux ONLY + +If you are running on a Linux operating system the default connection to the docker container `host.docker.internal` will not work. To connect to the local dockerized PostgreSQL DB, ensure there is a `.env-dev` file ([`.env-SAMPLE`](./.env-SAMPLE) can be used as a reference.) In the `.env-dev` file, ensure the `POSTGRES_HOST` variable is set to `172.17.0.1` + +```.env-dev +POSTGRES_HOST=172.17.0.1 +``` + +### Connecting to a different Database + +Alternatively, the app may be set up to connect to the existing staging database or another database. + +To connect to a different database (ie staging), the `.env-dev` file must also be used with values similar to: + +```.env-dev +POSTGRES_DB=iatlas_staging +POSTGRES_HOST=iatlas-staging-us-west-2.cluster-cfb68nhqxoz9.us-west-2.rds.amazonaws.com +POSTGRES_PASSWORD={Get_the_staging_password} +POSTGRES_USER=postgres +``` + +### Environment Variables + +All the environment variables used by the app have defaults. To set the environment variables, simply run the following bash script from the root of the project. (Please note the dot(`.`) at the very beginning of the command. This will "source" the script.): + +```sh +. set_env_variables.sh +``` + +The default environment variables' values may be over-written by adding the value to a `.env-dev` file in the root of the project. This file is not versioned in the repository. + +The [`.env-SAMPLE`](./.env-SAMPLE) file is an example of what the `.env-dev` could be like and may be used as a reference. + +## Testing + +All tests are in the [`tests/`](./tests/) folder. + +See: [TESTING.md](./tests/TESTING.md) in the [`tests/`](./tests/) folder + +## Performance Profiling + +See: [PROFILING.md](./api/telemetry/PROFILING.md) in the [`api/telemetry/`](./api/telemetry/) folder + +## Logging + +See: [LOGGING.md](./api/logger/LOGGING.md) in the [`api/logger/`](./api/logger/) folder diff --git a/apps/iatlas/api-gitlab/api/__init__.py b/apps/iatlas/api-gitlab/api/__init__.py new file mode 100644 index 0000000000..0589004318 --- /dev/null +++ b/apps/iatlas/api-gitlab/api/__init__.py @@ -0,0 +1,55 @@ +import logging +from json import loads +from datetime import datetime as dt +from flask import Flask, request +from config import get_config +from .extensions import db, logs + + +def create_app(test=False): + config = get_config(test=test) + app = Flask(__name__) + app.config.from_object(config) + + register_extensions(app) + + # Blueprint registration here. + from .main import bp as main_bp + app.register_blueprint(main_bp) + + @app.after_request + def after_request(response): + """ Logging after every POST request only if it isn't an introspection query. """ + json_data = request.get_json() + is_introspection_query = bool(json_data and json_data.get( + 'operationName', False) == 'IntrospectionQuery') + if request.method == 'POST' and not is_introspection_query: + logger = logging.getLogger('api.access') + logger.info( + '%s [%s] %s %s %s %s %s %s %s', + request.remote_addr, + dt.utcnow().strftime('%d/%b/%Y:%H:%M:%S.%f')[:-3], + request.method, + request.path, + request.scheme, + response.status, + response.content_length, + request.referrer, + request.user_agent, + ) + return response + + @ app.teardown_appcontext + def shutdown_session(exception=None): + db.session.remove() + + return app + + +def register_extensions(app): + db.init_app(app) + logs.init_app(app) + return None + + +from api import db_models diff --git a/apps/iatlas/api-gitlab/api/database/__init__.py b/apps/iatlas/api-gitlab/api/database/__init__.py new file mode 100644 index 0000000000..2063401c7d --- /dev/null +++ b/apps/iatlas/api-gitlab/api/database/__init__.py @@ -0,0 +1,34 @@ +from .cell_queries import * +from .cell_stat_queries import * +from .cell_to_feature_queries import * +from .cell_to_gene_queries import * +from .cell_to_sample_queries import * +from .cohort_queries import * +from .cohort_to_feature_queries import * +from .cohort_to_gene_queries import * +from .cohort_to_mutation_queries import * +from .cohort_to_sample_queries import * +from .cohort_to_tag_queries import * +from .dataset_queries import * +from .dataset_to_sample_queries import * +from .dataset_to_tag_queries import * +from .edge_queries import * +from .feature_queries import * +from .feature_to_sample_queries import * +from .gene_queries import * +from .gene_to_sample_queries import * +from .gene_to_gene_set_queries import * +from .mutation_queries import * +from .node_queries import * +from .patient_queries import * +from .publication_queries import * +from .publication_to_gene_to_gene_set_queries import * +from .single_cell_pseudobulk_queries import * +from .single_cell_pseudobulk_feature_queries import * +from .result_queries import * +from .sample_to_mutation_queries import * +from .sample_to_tag_queries import * +from .snp_queries import * +from .tag_queries import * +from .tag_to_publication_queries import * +from .tag_to_tag_queries import * diff --git a/apps/iatlas/api-gitlab/api/database/cell_queries.py b/apps/iatlas/api-gitlab/api/database/cell_queries.py new file mode 100644 index 0000000000..c35355982c --- /dev/null +++ b/apps/iatlas/api-gitlab/api/database/cell_queries.py @@ -0,0 +1,12 @@ +from sqlalchemy import orm +from api import db +from api.db_models import Cell +from .database_helpers import general_core_fields, build_general_query + +cell_core_fields = [ + 'id', 'name', 'cell_type'] + +def return_cell_query(*args): + return build_general_query( + Cell, args=args, + accepted_query_args=cell_core_fields) \ No newline at end of file diff --git a/apps/iatlas/api-gitlab/api/database/cell_stat_queries.py b/apps/iatlas/api-gitlab/api/database/cell_stat_queries.py new file mode 100644 index 0000000000..c4614077bc --- /dev/null +++ b/apps/iatlas/api-gitlab/api/database/cell_stat_queries.py @@ -0,0 +1,19 @@ +from api import db +from api.db_models import CellStat +from .database_helpers import build_general_query + +related_fields = ['data_set', 'gene'] + +core_fields = [ + 'id', + 'cell_type', + 'cell_count', + 'avg_expr', + 'perc_expr'] + + +def return_cell_stat_query(*args): + return build_general_query( + CellStat, args=args, + accepted_option_args=related_fields, + accepted_query_args=core_fields) diff --git a/apps/iatlas/api-gitlab/api/database/cell_to_feature_queries.py b/apps/iatlas/api-gitlab/api/database/cell_to_feature_queries.py new file mode 100644 index 0000000000..166adf7ab7 --- /dev/null +++ b/apps/iatlas/api-gitlab/api/database/cell_to_feature_queries.py @@ -0,0 +1,15 @@ +from sqlalchemy import orm +from api import db +from api.db_models import CellToFeature +from .database_helpers import build_general_query + +related_fields = ['feature', 'cell'] + +core_fields = ['feature_id', 'cell_id', 'feature_value'] + + +def return_cell_to_feature_query(*args): + return build_general_query( + CellToFeature, args=args, + accepted_option_args=related_fields, + accepted_query_args=core_fields) \ No newline at end of file diff --git a/apps/iatlas/api-gitlab/api/database/cell_to_gene_queries.py b/apps/iatlas/api-gitlab/api/database/cell_to_gene_queries.py new file mode 100644 index 0000000000..646cf945e3 --- /dev/null +++ b/apps/iatlas/api-gitlab/api/database/cell_to_gene_queries.py @@ -0,0 +1,15 @@ +from sqlalchemy import orm +from api import db +from api.db_models import CellToGene +from .database_helpers import build_general_query + +related_fields = ['gene', 'cell'] + +core_fields = ['gene_id', 'cell_id', 'single_cell_seq'] + + +def return_cell_to_gene_query(*args): + return build_general_query( + CellToGene, args=args, + accepted_option_args=related_fields, + accepted_query_args=core_fields) \ No newline at end of file diff --git a/apps/iatlas/api-gitlab/api/database/cell_to_sample_queries.py b/apps/iatlas/api-gitlab/api/database/cell_to_sample_queries.py new file mode 100644 index 0000000000..a0eaa23e2a --- /dev/null +++ b/apps/iatlas/api-gitlab/api/database/cell_to_sample_queries.py @@ -0,0 +1,15 @@ +from sqlalchemy import orm +from api import db +from api.db_models import CellToSample +from .database_helpers import build_general_query + +related_fields = ['sample', 'cell'] + +core_fields = ['sample_id', 'cell_id'] + + +def return_cell_to_sample_query(*args): + return build_general_query( + CellToSample, args=args, + accepted_option_args=related_fields, + accepted_query_args=core_fields) \ No newline at end of file diff --git a/apps/iatlas/api-gitlab/api/database/cohort_queries.py b/apps/iatlas/api-gitlab/api/database/cohort_queries.py new file mode 100644 index 0000000000..e8aac6556e --- /dev/null +++ b/apps/iatlas/api-gitlab/api/database/cohort_queries.py @@ -0,0 +1,16 @@ +from sqlalchemy import orm +from api import db +from .database_helpers import build_general_query +from api.db_models import Cohort + +cohort_related_fields = ['data_set', 'tag', + 'samples', 'features', 'mutations', 'genes'] + +cohort_core_fields = ['id', 'name', 'dataset_id', 'tag_id'] + + +def return_cohort_query(*args): + return build_general_query( + Cohort, args=args, + accepted_option_args=cohort_related_fields, + accepted_query_args=cohort_core_fields) diff --git a/apps/iatlas/api-gitlab/api/database/cohort_to_feature_queries.py b/apps/iatlas/api-gitlab/api/database/cohort_to_feature_queries.py new file mode 100644 index 0000000000..5f4f183f62 --- /dev/null +++ b/apps/iatlas/api-gitlab/api/database/cohort_to_feature_queries.py @@ -0,0 +1,16 @@ +from sqlalchemy import orm +from api import db +from api.db_models import CohortToFeature +from .database_helpers import build_general_query + +accepted_cohort_to_feature_option_args = ['cohort', 'feature'] + +accepted_cohort_to_feature_query_args = [ + 'cohort_id', 'feature_id'] + + +def return_cohort_to_feature_query(*args): + return build_general_query( + CohortToFeature, args=args, + accepted_option_args=accepted_cohort_to_feature_option_args, + accepted_query_args=accepted_cohort_to_feature_query_args) diff --git a/apps/iatlas/api-gitlab/api/database/cohort_to_gene_queries.py b/apps/iatlas/api-gitlab/api/database/cohort_to_gene_queries.py new file mode 100644 index 0000000000..e65a33172d --- /dev/null +++ b/apps/iatlas/api-gitlab/api/database/cohort_to_gene_queries.py @@ -0,0 +1,16 @@ +from sqlalchemy import orm +from api import db +from api.db_models import CohortToGene +from .database_helpers import build_general_query + +accepted_cohort_to_gene_option_args = ['cohort', 'gene'] + +accepted_cohort_to_gene_query_args = [ + 'cohort_id', 'gene_id'] + + +def return_cohort_to_gene_query(*args): + return build_general_query( + CohortToGene, args=args, + accepted_option_args=accepted_cohort_to_gene_option_args, + accepted_query_args=accepted_cohort_to_gene_query_args) diff --git a/apps/iatlas/api-gitlab/api/database/cohort_to_mutation_queries.py b/apps/iatlas/api-gitlab/api/database/cohort_to_mutation_queries.py new file mode 100644 index 0000000000..773860a4e9 --- /dev/null +++ b/apps/iatlas/api-gitlab/api/database/cohort_to_mutation_queries.py @@ -0,0 +1,16 @@ +from sqlalchemy import orm +from api import db +from api.db_models import CohortToMutation +from .database_helpers import build_general_query + +accepted_cohort_to_mutation_option_args = ['cohort', 'mutation'] + +accepted_cohort_to_mutation_query_args = [ + 'cohort_id', 'mutation_id'] + + +def return_cohort_to_mutation_query(*args): + return build_general_query( + CohortToMutation, args=args, + accepted_option_args=accepted_cohort_to_mutation_option_args, + accepted_query_args=accepted_cohort_to_mutation_query_args) diff --git a/apps/iatlas/api-gitlab/api/database/cohort_to_sample_queries.py b/apps/iatlas/api-gitlab/api/database/cohort_to_sample_queries.py new file mode 100644 index 0000000000..456a4233e1 --- /dev/null +++ b/apps/iatlas/api-gitlab/api/database/cohort_to_sample_queries.py @@ -0,0 +1,16 @@ +from sqlalchemy import orm +from api import db +from api.db_models import CohortToSample +from .database_helpers import build_general_query + +accepted_cohort_to_sample_option_args = ['cohort', 'sample'] + +accepted_cohort_to_sample_query_args = [ + 'cohort_id', 'sample_id' 'tag_id'] + + +def return_cohort_to_sample_query(*args): + return build_general_query( + CohortToSample, args=args, + accepted_option_args=accepted_cohort_to_sample_option_args, + accepted_query_args=accepted_cohort_to_sample_query_args) diff --git a/apps/iatlas/api-gitlab/api/database/cohort_to_tag_queries.py b/apps/iatlas/api-gitlab/api/database/cohort_to_tag_queries.py new file mode 100644 index 0000000000..ba0b409737 --- /dev/null +++ b/apps/iatlas/api-gitlab/api/database/cohort_to_tag_queries.py @@ -0,0 +1,16 @@ +from sqlalchemy import orm +from api import db +from api.db_models import CohortToTag +from .database_helpers import build_general_query + +accepted_cohort_to_tag_option_args = ['cohort', 'tag'] + +accepted_cohort_to_tag_query_args = [ + 'cohort_id', 'tag_id'] + + +def return_cohort_to_tag_query(*args): + return build_general_query( + CohortToTag, args=args, + accepted_option_args=accepted_cohort_to_tag_option_args, + accepted_query_args=accepted_cohort_to_tag_query_args) diff --git a/apps/iatlas/api-gitlab/api/database/database_helpers.py b/apps/iatlas/api-gitlab/api/database/database_helpers.py new file mode 100644 index 0000000000..fa4d4ab9f3 --- /dev/null +++ b/apps/iatlas/api-gitlab/api/database/database_helpers.py @@ -0,0 +1,68 @@ +from sqlalchemy import orm +from sqlalchemy.ext.compiler import compiles +from sqlalchemy.sql.expression import ClauseElement, Executable +from sqlalchemy.sql import Select +from sqlalchemy.dialects import postgresql + +from api import db + +general_core_fields = ['id', 'name'] + + +def build_general_query(model, args=[], accepted_option_args=[], accepted_query_args=[]): + option_args = build_option_args(*args, accepted_args=accepted_option_args) + query_args = build_query_args( + model, *args, accepted_args=accepted_query_args) + query = db.session.query(*query_args) + if option_args: + # If option args are found, the whole model must be queried. + return db.session.query(model).options(*option_args) + return db.session.query(*query_args) + + +def build_option_args(*args, accepted_args=[]): + option_args = [] + for arg in args: + if arg in accepted_args: + option_args.append(orm.joinedload(arg)) + return option_args + + +def build_query_args(model, *argv, accepted_args=[]): + query_args = [] + for arg in argv: + if arg in accepted_args: + query_args.append(getattr(model, arg)) + if not query_args: + return [model] + return query_args + +def temp_table(name, query): + e = db.session.get_bind() + c = e.connect() + trans = c.begin() + c.execute(CreateTableAs(name, query)) + trans.commit() + return c + +class CreateTableAs(Select): + def __init__(self, name, query, *arg, **kw): + super(CreateTableAs, self).__init__(None, *arg, **kw) + self.name = name + self.query = query + +@compiles(CreateTableAs) +def _create_table_as(element, compiler, **kw): + text = element.query.statement.compile(dialect=postgresql.dialect(), compile_kwargs={'literal_binds': True}) + query = "CREATE TEMP TABLE %s AS %s" % ( + element.name, + text + ) + return query + +def execute_sql(query, conn=None): + if conn: + return conn.execute(query) + engine = db.session.get_bind() + with engine.connect() as conn: + return conn.execute(query) diff --git a/apps/iatlas/api-gitlab/api/database/dataset_queries.py b/apps/iatlas/api-gitlab/api/database/dataset_queries.py new file mode 100644 index 0000000000..045cd83a84 --- /dev/null +++ b/apps/iatlas/api-gitlab/api/database/dataset_queries.py @@ -0,0 +1,16 @@ +from sqlalchemy import orm +from api import db +from api.db_models import Dataset +from .database_helpers import general_core_fields, build_general_query + +dataset_related_fields = [ + 'dataset_sample_assoc', 'dataset_tag_assoc', 'samples', 'tags'] + +dataset_core_fields = ['id', 'name', 'display', 'dataset_type'] + + +def return_dataset_query(*args, model=Dataset): + return build_general_query( + model, args=args, + accepted_option_args=dataset_related_fields, + accepted_query_args=dataset_core_fields) diff --git a/apps/iatlas/api-gitlab/api/database/dataset_to_sample_queries.py b/apps/iatlas/api-gitlab/api/database/dataset_to_sample_queries.py new file mode 100644 index 0000000000..b1f572e3f8 --- /dev/null +++ b/apps/iatlas/api-gitlab/api/database/dataset_to_sample_queries.py @@ -0,0 +1,15 @@ +from sqlalchemy import orm +from api import db +from api.db_models import DatasetToSample +from .database_helpers import build_general_query + +related_fields = ['data_sets', 'samples'] + +core_fields = ['dataset_id', 'sample_id'] + + +def return_dataset_to_sample_query(*args): + return build_general_query( + DatasetToSample, args=args, + accepted_option_args=related_fields, + accepted_query_args=core_fields) diff --git a/apps/iatlas/api-gitlab/api/database/dataset_to_tag_queries.py b/apps/iatlas/api-gitlab/api/database/dataset_to_tag_queries.py new file mode 100644 index 0000000000..23c1caebb3 --- /dev/null +++ b/apps/iatlas/api-gitlab/api/database/dataset_to_tag_queries.py @@ -0,0 +1,15 @@ +from sqlalchemy import orm +from api import db +from api.db_models import DatasetToTag +from .database_helpers import build_general_query + +related_fields = ['data_sets', 'tags'] + +core_fields = ['dataset_id', 'tag_id'] + + +def return_dataset_to_tag_query(*args): + return build_general_query( + DatasetToTag, args=args, + accepted_option_args=related_fields, + accepted_query_args=core_fields) diff --git a/apps/iatlas/api-gitlab/api/database/edge_queries.py b/apps/iatlas/api-gitlab/api/database/edge_queries.py new file mode 100644 index 0000000000..a3f2a8795e --- /dev/null +++ b/apps/iatlas/api-gitlab/api/database/edge_queries.py @@ -0,0 +1,15 @@ +from sqlalchemy import orm +from api import db +from api.db_models import Edge +from .database_helpers import build_general_query + +accepted_option_args = ['node_1', 'node_2'] + +accepted_query_args = ['id', 'node_1_id', + 'node_2_id', 'name', 'label', 'score'] + + +def return_edge_query(*args): + return build_general_query( + Edge, args=args, accepted_option_args=accepted_option_args, + accepted_query_args=accepted_query_args) diff --git a/apps/iatlas/api-gitlab/api/database/feature_queries.py b/apps/iatlas/api-gitlab/api/database/feature_queries.py new file mode 100644 index 0000000000..afde21eaec --- /dev/null +++ b/apps/iatlas/api-gitlab/api/database/feature_queries.py @@ -0,0 +1,17 @@ +from sqlalchemy import orm +from api import db +from api.db_models import Feature +from .database_helpers import general_core_fields, build_general_query + +feature_related_fields = [ + 'copy_number_results', 'driver_results', + 'feature_sample_assoc', 'samples'] + +feature_core_fields = [ + 'id', 'name', 'display', 'order', 'unit', 'feature_class', 'method_tag', 'germline_category', 'germline_module'] + +def return_feature_query(*args): + return build_general_query( + Feature, args=args, + accepted_option_args=feature_related_fields, + accepted_query_args=feature_core_fields) diff --git a/apps/iatlas/api-gitlab/api/database/feature_to_sample_queries.py b/apps/iatlas/api-gitlab/api/database/feature_to_sample_queries.py new file mode 100644 index 0000000000..bb2dbb1ff1 --- /dev/null +++ b/apps/iatlas/api-gitlab/api/database/feature_to_sample_queries.py @@ -0,0 +1,15 @@ +from sqlalchemy import orm +from api import db +from api.db_models import FeatureToSample +from .database_helpers import build_general_query + +related_fields = ['features', 'samples'] + +core_fields = ['feature_id', 'sample_id', 'value'] + + +def return_feature_to_sample_query(*args): + return build_general_query( + FeatureToSample, args=args, + accepted_option_args=related_fields, + accepted_query_args=core_fields) diff --git a/apps/iatlas/api-gitlab/api/database/gene_queries.py b/apps/iatlas/api-gitlab/api/database/gene_queries.py new file mode 100644 index 0000000000..fe010bad19 --- /dev/null +++ b/apps/iatlas/api-gitlab/api/database/gene_queries.py @@ -0,0 +1,43 @@ +from api import db +from api.db_models import Gene, GeneSet +from .database_helpers import general_core_fields, build_general_query + +gene_related_fields = ['copy_number_results', + 'driver_results', + 'gene_sample_assoc', + 'gene_set_assoc', + 'gene_sets', + 'publications', + 'publication_gene_gene_set_assoc', + 'samples'] + +gene_core_fields = ['id', + 'entrez_id', + 'hgnc_id', + 'description', + 'friendly_name', + 'io_landscape_name', + 'gene_family', + 'gene_function', + 'immune_checkpoint', + 'gene_pathway', + 'super_category', + 'therapy_type'] + +gene_set_related_fields = [ + 'genes', 'gene_set_assoc', 'publications', 'publication_gene_gene_set_assoc'] + +sub_related_fields = ['genes'] + + +def return_gene_query(*args, model=Gene): + return build_general_query( + model, args=args, + accepted_option_args=gene_related_fields, + accepted_query_args=gene_core_fields) + +def return_gene_set_query(*args): + return build_general_query( + GeneSet, args=args, + accepted_option_args=gene_set_related_fields, + accepted_query_args=general_core_fields) diff --git a/apps/iatlas/api-gitlab/api/database/gene_to_gene_set_queries.py b/apps/iatlas/api-gitlab/api/database/gene_to_gene_set_queries.py new file mode 100644 index 0000000000..b3e7b9913c --- /dev/null +++ b/apps/iatlas/api-gitlab/api/database/gene_to_gene_set_queries.py @@ -0,0 +1,15 @@ +from sqlalchemy import orm +from api import db +from api.db_models import GeneToGeneSet +from .database_helpers import build_general_query + +related_fields = ['genes', 'gene_sets'] + +core_fields = ['gene_id', 'gene_set_id'] + + +def return_gene_to_gene_set_query(*args): + return build_general_query( + GeneToGeneSet, args=args, + accepted_option_args=related_fields, + accepted_query_args=core_fields) diff --git a/apps/iatlas/api-gitlab/api/database/gene_to_sample_queries.py b/apps/iatlas/api-gitlab/api/database/gene_to_sample_queries.py new file mode 100644 index 0000000000..c50a18c723 --- /dev/null +++ b/apps/iatlas/api-gitlab/api/database/gene_to_sample_queries.py @@ -0,0 +1,15 @@ +from sqlalchemy import orm +from api import db +from api.db_models import GeneToSample +from .database_helpers import build_general_query + +related_fields = ['gene', 'sample'] + +core_fields = ['gene_id', 'sample_id', 'rna_seq_expression', 'nanostring_expression'] + + +def return_gene_to_sample_query(*args): + return build_general_query( + GeneToSample, args=args, + accepted_option_args=related_fields, + accepted_query_args=core_fields) diff --git a/apps/iatlas/api-gitlab/api/database/mutation_queries.py b/apps/iatlas/api-gitlab/api/database/mutation_queries.py new file mode 100644 index 0000000000..1a4fd2637b --- /dev/null +++ b/apps/iatlas/api-gitlab/api/database/mutation_queries.py @@ -0,0 +1,31 @@ +from sqlalchemy import orm +from api import db +from api.db_models import Mutation, MutationType +from .database_helpers import build_general_query, general_core_fields + +mutation_related_fields = [ + 'gene', 'mutation_type', 'sample_mutation_assoc', 'samples'] +mutation_core_fields = [ + 'id', 'name', 'gene_id', 'mutation_code', 'mutation_type_id'] + +mutation_code_related_fields = ['driver_results', 'mutations'] +mutation_code_core_fields = ['id', 'code'] + +mutation_type_related_fields = ['mutations'] +mutation_type_core_fields = general_core_fields + ['display'] + + +def return_mutation_query(*args): + return build_general_query( + Mutation, args=args, + accepted_option_args=mutation_related_fields, + accepted_query_args=mutation_core_fields) + + +def return_mutation_type_query(*args): + return build_general_query( + MutationType, args=args, + accepted_option_args=mutation_type_related_fields, + accepted_query_args=mutation_type_core_fields) + + diff --git a/apps/iatlas/api-gitlab/api/database/node_queries.py b/apps/iatlas/api-gitlab/api/database/node_queries.py new file mode 100644 index 0000000000..75cea71aae --- /dev/null +++ b/apps/iatlas/api-gitlab/api/database/node_queries.py @@ -0,0 +1,17 @@ +from api import db +from api.db_models import Node +from .database_helpers import build_general_query + +related_fields = [ + 'data_sets', 'edges_primary', 'edges_secondary', + 'feature', 'gene', 'tag1', 'tag2'] + +core_fields = ['id', 'dataset_id', 'feature_id', + 'gene_id', 'name', 'network', 'label', 'score', 'x', 'y'] + + +def return_node_query(*args): + return build_general_query( + Node, args=args, + accepted_option_args=related_fields, + accepted_query_args=core_fields) diff --git a/apps/iatlas/api-gitlab/api/database/patient_queries.py b/apps/iatlas/api-gitlab/api/database/patient_queries.py new file mode 100644 index 0000000000..1d0b47156c --- /dev/null +++ b/apps/iatlas/api-gitlab/api/database/patient_queries.py @@ -0,0 +1,48 @@ +from sqlalchemy import orm +from api import db +from api.db_models import Patient, Sample, Slide +from .database_helpers import build_general_query + +patient_related_fields = ['samples', 'slides'] + +patient_core_fields = [ + 'id', 'age_at_diagnosis', 'barcode', 'ethnicity', 'gender', 'height', 'race', 'weight'] + +sample_related_fields = ['data_sets', + 'dataset_sample_assoc', + 'feature_sample_assoc', + 'features', + 'gene_sample_assoc', + 'genes', + 'mutations', + 'patient', + 'sample_mutation_assoc', + 'sample_tag_assoc', + 'tags'] + +sample_core_fields = ['id', 'name', 'patient_id'] + +slide_related_fields = ['patient'] + +slide_core_fields = ['id', 'name', 'description'] + + +def return_patient_query(*args): + return build_general_query( + Patient, args=args, + accepted_option_args=patient_related_fields, + accepted_query_args=patient_core_fields) + + +def return_sample_query(*args): + return build_general_query( + Sample, args=args, + accepted_option_args=sample_related_fields, + accepted_query_args=sample_core_fields) + + +def return_slide_query(*args): + return build_general_query( + Slide, args=args, + accepted_option_args=slide_related_fields, + accepted_query_args=slide_core_fields) diff --git a/apps/iatlas/api-gitlab/api/database/publication_queries.py b/apps/iatlas/api-gitlab/api/database/publication_queries.py new file mode 100644 index 0000000000..e93576ad99 --- /dev/null +++ b/apps/iatlas/api-gitlab/api/database/publication_queries.py @@ -0,0 +1,19 @@ +from api import db +from api.db_models import Publication +from .database_helpers import build_general_query + +publication_related_fields = ['genes', + 'gene_sets', + 'publication_gene_gene_set_assoc', + 'tag_publication_assoc', + 'tags'] + +publication_core_fields = ['id', 'do_id', 'first_author_last_name', + 'journal', 'link', 'pubmed_id', 'title', 'year'] + + +def return_publication_query(*args): + return build_general_query( + Publication, args=args, + accepted_option_args=publication_related_fields, + accepted_query_args=publication_core_fields) diff --git a/apps/iatlas/api-gitlab/api/database/publication_to_gene_to_gene_set_queries.py b/apps/iatlas/api-gitlab/api/database/publication_to_gene_to_gene_set_queries.py new file mode 100644 index 0000000000..768c79c86e --- /dev/null +++ b/apps/iatlas/api-gitlab/api/database/publication_to_gene_to_gene_set_queries.py @@ -0,0 +1,15 @@ +from sqlalchemy import orm +from api import db +from api.db_models import PublicationToGeneToGeneSet +from .database_helpers import build_general_query + +related_fields = ['gene_sets', 'genes', 'publications'] + +core_fields = ['gene_id', 'gene_set_id', 'publication_id'] + + +def return_publication_to_gene_to_gene_set_query(*args, model=PublicationToGeneToGeneSet): + return build_general_query( + model, args=args, + accepted_option_args=related_fields, + accepted_query_args=core_fields) diff --git a/apps/iatlas/api-gitlab/api/database/result_queries.py b/apps/iatlas/api-gitlab/api/database/result_queries.py new file mode 100644 index 0000000000..09aa6e19f8 --- /dev/null +++ b/apps/iatlas/api-gitlab/api/database/result_queries.py @@ -0,0 +1,191 @@ +from api import db +from api.db_models import ( + Colocalization, + CopyNumberResult, + DriverResult, + Neoantigen, + HeritabilityResult, + GermlineGwasResult, + RareVariantPathwayAssociation +) +from .database_helpers import build_option_args, build_query_args + +accepted_coloc_option_args = [ + 'data_set', 'coloc_data_set', 'feature', 'gene', 'snp' +] + +accepted_coloc_query_args = [ + 'id', + 'dataset_id', + 'coloc_dataset_id', + 'feature_id', + 'gene_id', + 'snp_id', + 'qtl_type', + 'ecaviar_pp', + 'plot_type'] + +accepted_cnr_option_args = ['data_set', 'feature', 'gene', 'tag'] + +accepted_cnr_query_args = [ + 'id', + 'direction', + 'mean_normal', + 'mean_cnv', + 'p_value', + 'log10_p_value', + 't_stat', + 'dataset_id', + 'feature_id', + 'gene_id', + 'tag_id' +] + +accepted_dr_option_args = ['data_set', 'feature', 'tag', 'mutation'] + +accepted_dr_query_args = [ + 'id', + 'p_value', + 'fold_change', + 'log10_p_value', + 'log10_fold_change', + 'n_wt', + 'n_mut', + 'dataset_id', + 'feature_id', + 'mutation_id', + 'tag_id' +] + +accepted_hr_option_args = ['data_set', 'feature'] + +accepted_hr_query_args = [ + 'dataset_id', + 'id' + 'feature_id', + 'p_value', + 'cluster', + 'fdr', + 'variance', + 'se' +] + +accepted_ggr_option_args = ['data_set', 'feature', 'snp'] + +accepted_ggr_query_args = [ + 'dataset_id', + 'id' + 'feature_id', + 'snp_id', + 'p_value', + 'maf' +] + +accepted_neoantigen_option_args = ['patient', 'gene'] + +accepted_neoantigen_query_args = [ + 'id', + 'pmhc', + 'freq_pmhc', + 'tpm', + 'neoantiogen_gene_id', + 'patient_id', +] + +accepted_rvpa_option_args = ['data_set', 'feature'] + +accepted_rvpa_query_args = [ + 'id', + 'data_set_id', + 'feature_id', + 'pathway', + 'p_value', + 'min', + 'max', + 'mean', + 'q1', + 'q2', + 'q3', + 'n_mutants', + 'n_total' +] + + +def return_colocalization_query(*args): + option_args = build_option_args( + *args, accepted_args=accepted_coloc_option_args) + query_args = build_query_args( + Colocalization, * args, accepted_args=accepted_coloc_query_args) + query = db.session.query(*query_args) + if option_args: + query = db.session.query(Colocalization).options(*option_args) + return query + + +def return_copy_number_result_query(*args): + option_args = build_option_args( + *args, accepted_args=accepted_cnr_option_args) + query_args = build_query_args( + CopyNumberResult, * args, accepted_args=accepted_cnr_query_args) + query = db.session.query(*query_args) + if option_args: + query = db.session.query(CopyNumberResult).options(*option_args) + return query + + +def return_driver_result_query(*args): + option_args = build_option_args( + *args, accepted_args=accepted_dr_option_args) + query_args = build_query_args( + DriverResult, *args, accepted_args=accepted_dr_query_args) + query = db.session.query(*query_args) + if option_args: + query = db.session.query(DriverResult).options(*option_args) + return query + + +def return_heritability_result_query(*args): + option_args = build_option_args( + *args, accepted_args=accepted_hr_option_args) + query_args = build_query_args( + HeritabilityResult, *args, accepted_args=accepted_hr_query_args) + query = db.session.query(*query_args) + if option_args: + query = db.session.query(HeritabilityResult).options(*option_args) + return query + + +def return_germline_gwas_result_query(*args): + option_args = build_option_args( + *args, accepted_args=accepted_ggr_option_args) + query_args = build_query_args( + GermlineGwasResult, *args, accepted_args=accepted_ggr_query_args) + query = db.session.query(*query_args) + if option_args: + query = db.session.query(GermlineGwasResult).options(*option_args) + return query + + +def return_neoantigen_query(*args): + option_args = build_option_args( + *args, accepted_args=accepted_neoantigen_option_args + ) + query_args = build_query_args( + Neoantigen, *args, accepted_args=accepted_neoantigen_query_args + ) + query = db.session.query(*query_args) + if option_args: + query = db.session.query(Neoantigen).options(*option_args) + return query + + +def return_rare_variant_pathway_associations_query(*args): + option_args = build_option_args( + *args, accepted_args=accepted_rvpa_option_args) + query_args = build_query_args( + RareVariantPathwayAssociation, *args, accepted_args=accepted_rvpa_query_args) + query = db.session.query(*query_args) + if option_args: + query = db.session.query( + RareVariantPathwayAssociation).options(*option_args) + return query diff --git a/apps/iatlas/api-gitlab/api/database/sample_queries.py b/apps/iatlas/api-gitlab/api/database/sample_queries.py new file mode 100644 index 0000000000..9bce3dd064 --- /dev/null +++ b/apps/iatlas/api-gitlab/api/database/sample_queries.py @@ -0,0 +1,14 @@ +from api.db_models import Sample +from .database_helpers import build_general_query + +sample_related_fields = ['features', 'genes'] + +sample_core_fields = [ + 'id', 'name', 'patient_id'] + + +def return_sample_query(*args): + return build_general_query( + Sample, args=args, + accepted_option_args=sample_related_fields, + accepted_query_args=sample_core_fields) diff --git a/apps/iatlas/api-gitlab/api/database/sample_to_mutation_queries.py b/apps/iatlas/api-gitlab/api/database/sample_to_mutation_queries.py new file mode 100644 index 0000000000..230665ee65 --- /dev/null +++ b/apps/iatlas/api-gitlab/api/database/sample_to_mutation_queries.py @@ -0,0 +1,15 @@ +from sqlalchemy import orm +from api import db +from api.db_models import SampleToMutation +from .database_helpers import build_general_query + +related_fields = ['mutations', 'samples'] + +core_fields = ['mutation_id', 'sample_id'] + + +def return_sample_to_mutation_query(*args): + return build_general_query( + SampleToMutation, args=args, + accepted_option_args=related_fields, + accepted_query_args=core_fields) diff --git a/apps/iatlas/api-gitlab/api/database/sample_to_tag_queries.py b/apps/iatlas/api-gitlab/api/database/sample_to_tag_queries.py new file mode 100644 index 0000000000..13e18b1885 --- /dev/null +++ b/apps/iatlas/api-gitlab/api/database/sample_to_tag_queries.py @@ -0,0 +1,15 @@ +from sqlalchemy import orm +from api import db +from api.db_models import SampleToTag +from .database_helpers import build_general_query + +related_fields = ['samples', 'tags'] + +core_fields = ['sample_id', 'tag_id'] + + +def return_sample_to_tag_query(*args): + return build_general_query( + SampleToTag, args=args, + accepted_option_args=related_fields, + accepted_query_args=core_fields) diff --git a/apps/iatlas/api-gitlab/api/database/single_cell_pseudobulk_feature_queries.py b/apps/iatlas/api-gitlab/api/database/single_cell_pseudobulk_feature_queries.py new file mode 100644 index 0000000000..b6c9a34901 --- /dev/null +++ b/apps/iatlas/api-gitlab/api/database/single_cell_pseudobulk_feature_queries.py @@ -0,0 +1,15 @@ +from sqlalchemy import orm +from api import db +from api.db_models import SingleCellPseudobulkFeature +from .database_helpers import build_general_query + +related_fields = ['feature', 'sample'] + +core_fields = ['id', 'feature_id', 'sample_id', 'value', 'cell_type'] + + +def return_single_cell_pseudobulk_feature_query(*args): + return build_general_query( + SingleCellPseudobulkFeature, args=args, + accepted_option_args=related_fields, + accepted_query_args=core_fields) \ No newline at end of file diff --git a/apps/iatlas/api-gitlab/api/database/single_cell_pseudobulk_queries.py b/apps/iatlas/api-gitlab/api/database/single_cell_pseudobulk_queries.py new file mode 100644 index 0000000000..55b92d015c --- /dev/null +++ b/apps/iatlas/api-gitlab/api/database/single_cell_pseudobulk_queries.py @@ -0,0 +1,15 @@ +from sqlalchemy import orm +from api import db +from api.db_models import SingleCellPseudobulk +from .database_helpers import build_general_query + +related_fields = ['gene', 'sample'] + +core_fields = ['id', 'gene_id', 'sample_id', 'single_cell_seq_sum', 'cell_type'] + + +def return_single_cell_pseudobulk_query(*args): + return build_general_query( + SingleCellPseudobulk, args=args, + accepted_option_args=related_fields, + accepted_query_args=core_fields) \ No newline at end of file diff --git a/apps/iatlas/api-gitlab/api/database/slide_queries.py b/apps/iatlas/api-gitlab/api/database/slide_queries.py new file mode 100644 index 0000000000..dc1df3278a --- /dev/null +++ b/apps/iatlas/api-gitlab/api/database/slide_queries.py @@ -0,0 +1,15 @@ +from flaskr.db_models import Slide +from .database_helpers import general_core_fields, build_general_query + +slide_related_fields = [ + 'patient'] + +slide_core_fields = [ + 'id', 'name', 'description', 'patient_id'] + + +def return_slide_query(*args): + return build_general_query( + Slide, args=args, + accepted_option_args=slide_related_fields, + accepted_query_args=slide_core_fields) diff --git a/apps/iatlas/api-gitlab/api/database/snp_queries.py b/apps/iatlas/api-gitlab/api/database/snp_queries.py new file mode 100644 index 0000000000..dd57765967 --- /dev/null +++ b/apps/iatlas/api-gitlab/api/database/snp_queries.py @@ -0,0 +1,13 @@ +from sqlalchemy import orm +from api import db +from .database_helpers import general_core_fields, build_general_query +from api.db_models import Snp + +snp_core_fields = [ + 'id', 'name', 'rsid', 'chr', 'bp'] + + +def return_snp_query(*args): + return build_general_query( + Snp, args=args, + accepted_query_args=snp_core_fields) diff --git a/apps/iatlas/api-gitlab/api/database/tag_queries.py b/apps/iatlas/api-gitlab/api/database/tag_queries.py new file mode 100644 index 0000000000..b4cbff2fbb --- /dev/null +++ b/apps/iatlas/api-gitlab/api/database/tag_queries.py @@ -0,0 +1,33 @@ +from sqlalchemy import orm +from api import db +from api.db_models import Tag +from .database_helpers import build_general_query + +related_fields = ['copy_number_results', + 'data_sets', + 'dataset_tag_assoc', + 'driver_results', + 'node_tag_assoc', + 'nodes', + 'publications', + 'related_tags', + 'sample_tag_assoc', + 'samples', + 'tag_publication_assoc', + 'tags'] + +core_fields = ['id', + 'characteristics', + 'color', + 'long_display', + 'name', + 'short_display', + 'type', + 'order'] + + +def return_tag_query(*args, model=Tag): + return build_general_query( + model, args=args, + accepted_option_args=related_fields, + accepted_query_args=core_fields) diff --git a/apps/iatlas/api-gitlab/api/database/tag_to_publication_queries.py b/apps/iatlas/api-gitlab/api/database/tag_to_publication_queries.py new file mode 100644 index 0000000000..489db82a80 --- /dev/null +++ b/apps/iatlas/api-gitlab/api/database/tag_to_publication_queries.py @@ -0,0 +1,15 @@ +from sqlalchemy import orm +from api import db +from api.db_models import TagToPublication +from .database_helpers import build_general_query + +related_fields = ['publications', 'tags'] + +core_fields = ['publication_id', 'tag_id'] + + +def return_tag_to_publication_query(*args): + return build_general_query( + TagToPublication, args=args, + accepted_option_args=related_fields, + accepted_query_args=core_fields) diff --git a/apps/iatlas/api-gitlab/api/database/tag_to_tag_queries.py b/apps/iatlas/api-gitlab/api/database/tag_to_tag_queries.py new file mode 100644 index 0000000000..8964cb9861 --- /dev/null +++ b/apps/iatlas/api-gitlab/api/database/tag_to_tag_queries.py @@ -0,0 +1,15 @@ +from sqlalchemy import orm +from api import db +from api.db_models import TagToTag +from .database_helpers import build_general_query + +related_fields = ['related_tags', 'tags'] + +core_fields = ['related_tag_id', 'tag_id'] + + +def return_tag_to_tag_query(*args, model=TagToTag): + return build_general_query( + model, args=args, + accepted_option_args=related_fields, + accepted_query_args=core_fields) diff --git a/apps/iatlas/api-gitlab/api/db_models/__init__.py b/apps/iatlas/api-gitlab/api/db_models/__init__.py new file mode 100644 index 0000000000..1d7294dc90 --- /dev/null +++ b/apps/iatlas/api-gitlab/api/db_models/__init__.py @@ -0,0 +1,48 @@ +from api import db + +Base = db.Model + +from .cell import Cell +from .cell_stat import CellStat +from .cell_to_feature import CellToFeature +from .cell_to_gene import CellToGene +from .cell_to_sample import CellToSample +from .cohort import Cohort +from .cohort_to_gene import CohortToGene +from .cohort_to_feature import CohortToFeature +from .cohort_to_mutation import CohortToMutation +from .cohort_to_sample import CohortToSample +from .cohort_to_tag import CohortToTag +from .colocalization import Colocalization +from .copy_number_result import CopyNumberResult +from .dataset import Dataset +from .dataset_to_sample import DatasetToSample +from .dataset_to_tag import DatasetToTag +from .driver_result import DriverResult +from .edge import Edge +from .feature import Feature +from .feature_to_sample import FeatureToSample +from .gene import Gene +from .gene_to_sample import GeneToSample +from .gene_to_gene_set import GeneToGeneSet +from .gene_set import GeneSet +from .germline_gwas_result import GermlineGwasResult +from .heritability_result import HeritabilityResult +from .mutation import Mutation +from .mutation_type import MutationType +from .neoantigen import Neoantigen +from .node import Node +from .patient import Patient +from .publication import Publication +from .publication_to_gene_to_gene_set import PublicationToGeneToGeneSet +from .single_cell_pseudobulk import SingleCellPseudobulk +from .single_cell_pseudobulk_feature import SingleCellPseudobulkFeature +from .rare_variant_pathway_associations import RareVariantPathwayAssociation +from .sample import Sample +from .sample_to_mutation import SampleToMutation +from .sample_to_tag import SampleToTag +from .slide import Slide +from .snp import Snp +from .tag import Tag +from .tag_to_publication import TagToPublication +from .tag_to_tag import TagToTag diff --git a/apps/iatlas/api-gitlab/api/db_models/cell.py b/apps/iatlas/api-gitlab/api/db_models/cell.py new file mode 100644 index 0000000000..6185c9ac0a --- /dev/null +++ b/apps/iatlas/api-gitlab/api/db_models/cell.py @@ -0,0 +1,13 @@ +from sqlalchemy import orm +from api import db +from . import Base + + +class Cell(Base): + __tablename__ = 'cells' + id = db.Column(db.String, primary_key=True) + name = db.Column(db.String, nullable=False) + cell_type = db.Column(db.String, nullable=False) + + def __repr__(self): + return '' % self.name diff --git a/apps/iatlas/api-gitlab/api/db_models/cell_stat.py b/apps/iatlas/api-gitlab/api/db_models/cell_stat.py new file mode 100644 index 0000000000..ae32386f54 --- /dev/null +++ b/apps/iatlas/api-gitlab/api/db_models/cell_stat.py @@ -0,0 +1,30 @@ +from sqlalchemy import orm +from api import db +from . import Base + + +class CellStat(Base): + __tablename__ = 'cell_stats' + id = db.Column(db.String, primary_key=True) + cell_type = db.Column(db.String, nullable=False) + cell_count = db.Column(db.Integer, nullable=True) + avg_expr = db.Column(db.Numeric, nullable=True) + perc_expr = db.Column(db.Numeric, nullable=True) + + + dataset_id = db.Column(db.String, db.ForeignKey( + 'datasets.id'), nullable=False) + + gene_id = db.Column(db.String, db.ForeignKey( + 'genes.id'), nullable=False) + + data_set = db.relationship( + 'Dataset', backref=orm.backref('cell_stats', uselist=True, lazy='noload'), + uselist=False, lazy='noload') + + gene = db.relationship( + 'Gene', backref=orm.backref('cell_stats', uselist=True, lazy='noload'), + uselist=False, lazy='noload') + + def __repr__(self): + return '' % self.id \ No newline at end of file diff --git a/apps/iatlas/api-gitlab/api/db_models/cell_to_feature.py b/apps/iatlas/api-gitlab/api/db_models/cell_to_feature.py new file mode 100644 index 0000000000..93bbd7e69b --- /dev/null +++ b/apps/iatlas/api-gitlab/api/db_models/cell_to_feature.py @@ -0,0 +1,25 @@ +from sqlalchemy import orm +from api import db +from . import Base + + +class CellToFeature(Base): + __tablename__ = 'cells_to_features' + + id = db.Column(db.String, primary_key=True) + feature_value = db.Column(db.Numeric, nullable=False) + + feature_id = db.Column(db.String, db.ForeignKey( + 'features.id'), primary_key=True) + + cell_id = db.Column(db.String, db.ForeignKey( + 'cells.id'), primary_key=True) + + feature = db.relationship('Feature', backref=orm.backref( + 'feature_cell_assoc', uselist=True, lazy='noload'), uselist=False, lazy='noload') + + cell = db.relationship('Cell', backref=orm.backref( + 'feature_cell_assoc', uselist=True, lazy='noload'), uselist=False, lazy='noload') + + def __repr__(self): + return '' % self.id diff --git a/apps/iatlas/api-gitlab/api/db_models/cell_to_gene.py b/apps/iatlas/api-gitlab/api/db_models/cell_to_gene.py new file mode 100644 index 0000000000..cd8e0985bd --- /dev/null +++ b/apps/iatlas/api-gitlab/api/db_models/cell_to_gene.py @@ -0,0 +1,25 @@ +from sqlalchemy import orm +from api import db +from . import Base + + +class CellToGene(Base): + __tablename__ = 'cells_to_genes' + + id = db.Column(db.String, primary_key=True) + single_cell_seq = db.Column(db.Numeric, nullable=False) + + gene_id = db.Column(db.String, db.ForeignKey( + 'genes.id'), primary_key=True) + + cell_id = db.Column(db.String, db.ForeignKey( + 'cells.id'), primary_key=True) + + gene = db.relationship('Gene', backref=orm.backref( + 'gene_cell_assoc', uselist=True, lazy='noload'), uselist=False, lazy='noload') + + cell = db.relationship('Cell', backref=orm.backref( + 'gene_cell_assoc', uselist=True, lazy='noload'), uselist=False, lazy='noload') + + def __repr__(self): + return '' % self.id \ No newline at end of file diff --git a/apps/iatlas/api-gitlab/api/db_models/cell_to_sample.py b/apps/iatlas/api-gitlab/api/db_models/cell_to_sample.py new file mode 100644 index 0000000000..d7ad18a68b --- /dev/null +++ b/apps/iatlas/api-gitlab/api/db_models/cell_to_sample.py @@ -0,0 +1,24 @@ +from sqlalchemy import orm +from api import db +from . import Base + + +class CellToSample(Base): + __tablename__ = 'cells_to_samples' + + id = db.Column(db.String, primary_key=True) + + sample_id = db.Column(db.String, db.ForeignKey( + 'samples.id'), primary_key=True) + + cell_id = db.Column(db.String, db.ForeignKey( + 'cells.id'), primary_key=True) + + sample = db.relationship('Sample', backref=orm.backref( + 'sample_cell_assoc', uselist=True, lazy='noload'), uselist=False, lazy='noload') + + cell = db.relationship('Cell', backref=orm.backref( + 'sample_cell_assoc', uselist=True, lazy='noload'), uselist=False, lazy='noload') + + def __repr__(self): + return '' % self.id \ No newline at end of file diff --git a/apps/iatlas/api-gitlab/api/db_models/cohort.py b/apps/iatlas/api-gitlab/api/db_models/cohort.py new file mode 100644 index 0000000000..9bbc8c045f --- /dev/null +++ b/apps/iatlas/api-gitlab/api/db_models/cohort.py @@ -0,0 +1,37 @@ +from sqlalchemy import orm +from api import db +from . import Base + + +class Cohort(Base): + __tablename__ = 'cohorts' + id = db.Column(db.String, primary_key=True) + name = db.Column(db.String, nullable=False) + + dataset_id = db.Column(db.String, db.ForeignKey( + 'datasets.id'), nullable=False) + + cohort_tag_id = db.Column(db.String, db.ForeignKey('tags.id'), nullable=False) + + data_set = db.relationship( + 'Dataset', backref=orm.backref('cohorts', uselist=True, lazy='noload'), + uselist=False, lazy='noload') + + tag = db.relationship( + 'Tag', backref=orm.backref('cohorts', uselist=True, lazy='noload'), + uselist=False, lazy='noload') + + samples = db.relationship( + "Sample", secondary='cohorts_to_samples', uselist=True, lazy='noload') + + features = db.relationship( + "Feature", secondary='cohorts_to_features', uselist=True, lazy='noload') + + genes = db.relationship( + "Gene", secondary='cohorts_to_genes', uselist=True, lazy='noload') + + mutations = db.relationship( + "Mutation", secondary='cohorts_to_mutations', uselist=True, lazy='noload') + + def __repr__(self): + return '' % self.name diff --git a/apps/iatlas/api-gitlab/api/db_models/cohort_to_feature.py b/apps/iatlas/api-gitlab/api/db_models/cohort_to_feature.py new file mode 100644 index 0000000000..3354f6ac0d --- /dev/null +++ b/apps/iatlas/api-gitlab/api/db_models/cohort_to_feature.py @@ -0,0 +1,24 @@ +from sqlalchemy import orm +from api import db +from . import Base + + +class CohortToFeature(Base): + __tablename__ = 'cohorts_to_features' + + id = db.Column(db.String, primary_key=True) + + cohort_id = db.Column(db.String, db.ForeignKey( + 'cohorts.id'), primary_key=True) + + feature_id = db.Column(db.String, db.ForeignKey( + 'features.id'), primary_key=True) + + cohort = db.relationship('Cohort', backref=orm.backref( + 'cohort_feature_assoc', uselist=True, lazy='noload'), uselist=False, lazy='noload') + + feature = db.relationship('Feature', backref=orm.backref( + 'cohort_feature_assoc', uselist=True, lazy='noload'), uselist=False, lazy='noload') + + def __repr__(self): + return '' % self.id diff --git a/apps/iatlas/api-gitlab/api/db_models/cohort_to_gene.py b/apps/iatlas/api-gitlab/api/db_models/cohort_to_gene.py new file mode 100644 index 0000000000..d6bfef2c2e --- /dev/null +++ b/apps/iatlas/api-gitlab/api/db_models/cohort_to_gene.py @@ -0,0 +1,24 @@ +from sqlalchemy import orm +from api import db +from . import Base + + +class CohortToGene(Base): + __tablename__ = 'cohorts_to_genes' + + id = db.Column(db.String, primary_key=True) + + cohort_id = db.Column(db.String, db.ForeignKey( + 'cohorts.id'), primary_key=True) + + gene_id = db.Column(db.String, db.ForeignKey( + 'genes.id'), primary_key=True) + + cohort = db.relationship('Cohort', backref=orm.backref( + 'cohort_gene_assoc', uselist=True, lazy='noload'), uselist=False, lazy='noload') + + gene = db.relationship('Gene', backref=orm.backref( + 'cohort_gene_assoc', uselist=True, lazy='noload'), uselist=False, lazy='noload') + + def __repr__(self): + return '' % self.id diff --git a/apps/iatlas/api-gitlab/api/db_models/cohort_to_mutation.py b/apps/iatlas/api-gitlab/api/db_models/cohort_to_mutation.py new file mode 100644 index 0000000000..d2d7c14cf8 --- /dev/null +++ b/apps/iatlas/api-gitlab/api/db_models/cohort_to_mutation.py @@ -0,0 +1,24 @@ +from sqlalchemy import orm +from api import db +from . import Base + + +class CohortToMutation(Base): + __tablename__ = 'cohorts_to_mutations' + + id = db.Column(db.String, primary_key=True) + + cohort_id = db.Column(db.String, db.ForeignKey( + 'cohorts.id'), primary_key=True) + + mutation_id = db.Column(db.String, db.ForeignKey( + 'mutations.id'), primary_key=True) + + cohort = db.relationship('Cohort', backref=orm.backref( + 'cohort_mutation_assoc', uselist=True, lazy='noload'), uselist=False, lazy='noload') + + mutation = db.relationship('Mutation', backref=orm.backref( + 'cohort_mutation_assoc', uselist=True, lazy='noload'), uselist=False, lazy='noload') + + def __repr__(self): + return '' % self.id diff --git a/apps/iatlas/api-gitlab/api/db_models/cohort_to_sample.py b/apps/iatlas/api-gitlab/api/db_models/cohort_to_sample.py new file mode 100644 index 0000000000..ee41240c5d --- /dev/null +++ b/apps/iatlas/api-gitlab/api/db_models/cohort_to_sample.py @@ -0,0 +1,27 @@ +from sqlalchemy import orm +from api import db +from . import Base + + +class CohortToSample(Base): + __tablename__ = 'cohorts_to_samples' + + id = db.Column(db.String, primary_key=True) + + cohort_id = db.Column(db.String, db.ForeignKey( + 'cohorts.id'), primary_key=True) + + sample_id = db.Column(db.String, db.ForeignKey( + 'samples.id'), primary_key=True) + + cohorts_to_samples_tag_id = db.Column(db.Integer, db.ForeignKey( + 'tags.id'), primary_key=True) + + cohort = db.relationship('Cohort', backref=orm.backref( + 'cohort_sample_assoc', uselist=True, lazy='noload'), uselist=False, lazy='noload') + + sample = db.relationship('Sample', backref=orm.backref( + 'cohort_sample_assoc', uselist=True, lazy='noload'), uselist=False, lazy='noload') + + def __repr__(self): + return '' % self.id diff --git a/apps/iatlas/api-gitlab/api/db_models/cohort_to_tag.py b/apps/iatlas/api-gitlab/api/db_models/cohort_to_tag.py new file mode 100644 index 0000000000..e512442a54 --- /dev/null +++ b/apps/iatlas/api-gitlab/api/db_models/cohort_to_tag.py @@ -0,0 +1,24 @@ +from sqlalchemy import orm +from api import db +from . import Base + + +class CohortToTag(Base): + __tablename__ = 'cohorts_to_tags' + + id = db.Column(db.String, primary_key=True) + + cohort_id = db.Column(db.String, db.ForeignKey( + 'cohorts.id'), primary_key=True) + + tag_id = db.Column(db.String, db.ForeignKey( + 'tags.id'), primary_key=True) + + cohort = db.relationship('Cohort', backref=orm.backref( + 'cohort_tag_assoc', uselist=True, lazy='noload'), uselist=False, lazy='noload') + + tag = db.relationship('Tag', backref=orm.backref( + 'cohort_tag_assoc', uselist=True, lazy='noload'), uselist=False, lazy='noload') + + def __repr__(self): + return '' % self.id diff --git a/apps/iatlas/api-gitlab/api/db_models/colocalization.py b/apps/iatlas/api-gitlab/api/db_models/colocalization.py new file mode 100644 index 0000000000..51fcf0fd33 --- /dev/null +++ b/apps/iatlas/api-gitlab/api/db_models/colocalization.py @@ -0,0 +1,51 @@ +from sqlalchemy import orm +from api import db +from . import Base +from api.enums import qtl_enum, ecaviar_pp_enum, coloc_plot_type_enum + + +class Colocalization(Base): + __tablename__ = 'colocalizations' + id = db.Column(db.String, primary_key=True) + qtl_type = db.Column(qtl_enum, nullable=False) + ecaviar_pp = db.Column(ecaviar_pp_enum, nullable=True) + plot_type = db.Column(coloc_plot_type_enum, nullable=True) + tissue = db.Column(db.String, nullable=True) + splice_loc = db.Column(db.String, nullable=True) + link = db.Column(db.String, nullable=False) + + dataset_id = db.Column(db.String, db.ForeignKey( + 'datasets.id'), nullable=False) + + coloc_dataset_id = db.Column(db.String, db.ForeignKey( + 'datasets.id'), nullable=False) + + feature_id = db.Column(db.String, db.ForeignKey( + 'features.id'), nullable=False) + + gene_id = db.Column(db.String, db.ForeignKey('genes.id'), nullable=False) + + snp_id = db.Column(db.String, db.ForeignKey('snps.id'), nullable=False) + + data_set = db.relationship( + 'Dataset', backref=orm.backref('colocalizations_primary', uselist=True, lazy='noload'), + uselist=False, lazy='noload', primaryjoin='Dataset.id==Colocalization.dataset_id') + + coloc_data_set = db.relationship( + 'Dataset', backref=orm.backref('colocalizations_secondary', uselist=True, lazy='noload'), + uselist=False, lazy='noload', primaryjoin='Dataset.id==Colocalization.coloc_dataset_id') + + feature = db.relationship( + 'Feature', backref=orm.backref('colocalizations', uselist=True, lazy='noload'), + uselist=False, lazy='noload') + + gene = db.relationship( + 'Gene', backref=orm.backref('colocalizations', uselist=True, lazy='noload'), + uselist=False, lazy='noload') + + snp = db.relationship( + 'Snp', backref=orm.backref('colocalizations', uselist=True, lazy='noload'), + uselist=False, lazy='noload') + + def __repr__(self): + return '' % self.id diff --git a/apps/iatlas/api-gitlab/api/db_models/copy_number_result.py b/apps/iatlas/api-gitlab/api/db_models/copy_number_result.py new file mode 100644 index 0000000000..6a44bba5e0 --- /dev/null +++ b/apps/iatlas/api-gitlab/api/db_models/copy_number_result.py @@ -0,0 +1,40 @@ +from sqlalchemy import orm +from api import db +from . import Base +from api.enums import direction_enum + + +class CopyNumberResult(Base): + __tablename__ = 'copy_number_results' + id = db.Column(db.String, primary_key=True) + direction = db.Column(direction_enum, nullable=False) + mean_normal = db.Column(db.Numeric, nullable=True) + mean_cnv = db.Column(db.Numeric, nullable=True) + p_value = db.Column(db.Numeric, nullable=True) + log10_p_value = db.Column(db.Numeric, nullable=True) + t_stat = db.Column(db.Numeric, nullable=True) + + dataset_id = db.Column(db.String, db.ForeignKey( + 'datasets.id'), nullable=False) + + feature_id = db.Column(db.String, db.ForeignKey( + 'features.id'), nullable=False) + + gene_id = db.Column(db.String, db.ForeignKey('genes.id'), nullable=False) + + tag_id = db.Column(db.String, db.ForeignKey('tags.id'), nullable=False) + + data_set = db.relationship('Dataset', backref=orm.backref( + 'copy_number_results', uselist=True, lazy='noload'), uselist=False, lazy='noload') + + feature = db.relationship('Feature', backref=orm.backref( + 'copy_number_results', uselist=True, lazy='noload'), uselist=False, lazy='noload') + + gene = db.relationship('Gene', backref=orm.backref( + 'copy_number_results', uselist=True, lazy='noload'), uselist=False, lazy='noload') + + tag = db.relationship('Tag', backref=orm.backref( + 'copy_number_results', uselist=True, lazy='noload'), uselist=False, lazy='noload') + + def __repr__(self): + return '' % self.id diff --git a/apps/iatlas/api-gitlab/api/db_models/dataset.py b/apps/iatlas/api-gitlab/api/db_models/dataset.py new file mode 100644 index 0000000000..fb8f001a47 --- /dev/null +++ b/apps/iatlas/api-gitlab/api/db_models/dataset.py @@ -0,0 +1,19 @@ +from api import db +from . import Base + + +class Dataset(Base): + __tablename__ = 'datasets' + id = db.Column(db.String, primary_key=True) + name = db.Column(db.String, nullable=False) + display = db.Column(db.String, nullable=True) + dataset_type = db.Column(db.String, nullable=False) + + samples = db.relationship( + 'Sample', secondary='datasets_to_samples', uselist=True, lazy='noload') + + tags = db.relationship( + 'Tag', secondary='datasets_to_tags', uselist=True, lazy='noload') + + def __repr__(self): + return '' % self.name diff --git a/apps/iatlas/api-gitlab/api/db_models/dataset_to_sample.py b/apps/iatlas/api-gitlab/api/db_models/dataset_to_sample.py new file mode 100644 index 0000000000..059ffb34db --- /dev/null +++ b/apps/iatlas/api-gitlab/api/db_models/dataset_to_sample.py @@ -0,0 +1,22 @@ +from sqlalchemy import orm +from api import db +from . import Base + + +class DatasetToSample(Base): + __tablename__ = 'datasets_to_samples' + + dataset_id = db.Column( + db.String, db.ForeignKey('datasets.id'), primary_key=True) + + sample_id = db.Column( + db.String, db.ForeignKey('samples.id'), nullable=False) + + data_sets = db.relationship('Dataset', backref=orm.backref( + 'dataset_sample_assoc', uselist=True, lazy='noload'), uselist=True, lazy='noload') + + samples = db.relationship('Sample', backref=orm.backref( + 'dataset_sample_assoc', uselist=True, lazy='noload'), uselist=True, lazy='noload') + + def __repr__(self): + return '' % self.dataset_id diff --git a/apps/iatlas/api-gitlab/api/db_models/dataset_to_tag.py b/apps/iatlas/api-gitlab/api/db_models/dataset_to_tag.py new file mode 100644 index 0000000000..a24291b67c --- /dev/null +++ b/apps/iatlas/api-gitlab/api/db_models/dataset_to_tag.py @@ -0,0 +1,22 @@ +from sqlalchemy import orm +from api import db +from . import Base + + +class DatasetToTag(Base): + __tablename__ = 'datasets_to_tags' + + dataset_id = db.Column( + db.String, db.ForeignKey('datasets.id'), primary_key=True) + + tag_id = db.Column( + db.String, db.ForeignKey('tags.id'), nullable=False) + + data_sets = db.relationship('Dataset', backref=orm.backref( + 'dataset_tag_assoc', uselist=True, lazy='noload'), uselist=True, lazy='noload') + + tags = db.relationship('Tag', backref=orm.backref( + 'dataset_tag_assoc', uselist=True, lazy='noload'), uselist=True, lazy='noload') + + def __repr__(self): + return '' % self.dataset_id diff --git a/apps/iatlas/api-gitlab/api/db_models/driver_result.py b/apps/iatlas/api-gitlab/api/db_models/driver_result.py new file mode 100644 index 0000000000..e196a8ba1b --- /dev/null +++ b/apps/iatlas/api-gitlab/api/db_models/driver_result.py @@ -0,0 +1,44 @@ +from sqlalchemy import orm +from api import db +from . import Base + + +class DriverResult(Base): + __tablename__ = 'driver_results' + id = db.Column(db.String, primary_key=True) + p_value = db.Column(db.Numeric, nullable=True) + fold_change = db.Column(db.Numeric, nullable=True) + log10_p_value = db.Column(db.Numeric, nullable=True) + log10_fold_change = db.Column(db.Numeric, nullable=True) + n_wildtype = db.Column(db.Integer, nullable=True) + n_mutants = db.Column(db.Integer, nullable=True) + + dataset_id = db.Column(db.String, db.ForeignKey( + 'datasets.id'), nullable=False) + + feature_id = db.Column(db.String, db.ForeignKey( + 'features.id'), nullable=False) + + mutation_id = db.Column(db.String, db.ForeignKey( + 'mutations.id'), nullable=False) + + tag_id = db.Column(db.String, db.ForeignKey('tags.id'), nullable=False) + + data_set = db.relationship( + 'Dataset', backref=orm.backref('driver_results', uselist=True, lazy='noload'), + uselist=False, lazy='noload') + + feature = db.relationship( + 'Feature', backref=orm.backref('driver_results', uselist=True, lazy='noload'), + uselist=False, lazy='noload') + + mutation = db.relationship( + 'Mutation', backref=orm.backref('driver_results', uselist=True, lazy='noload'), + uselist=False, lazy='noload') + + tag = db.relationship( + 'Tag', backref=orm.backref('driver_results', uselist=True, lazy='noload'), + uselist=False, lazy='noload') + + def __repr__(self): + return '' % self.id diff --git a/apps/iatlas/api-gitlab/api/db_models/edge.py b/apps/iatlas/api-gitlab/api/db_models/edge.py new file mode 100644 index 0000000000..e9819d5fc5 --- /dev/null +++ b/apps/iatlas/api-gitlab/api/db_models/edge.py @@ -0,0 +1,29 @@ +from sqlalchemy import orm +from api import db +from . import Base + + +class Edge(Base): + __tablename__ = 'edges' + id = db.Column(db.String, primary_key=True) + + node_1_id = db.Column( + db.String, db.ForeignKey('nodes.id'), nullable=False) + + node_2_id = db.Column( + db.String, db.ForeignKey('nodes.id'), nullable=False) + + label = db.Column(db.String, nullable=True) + name = db.Column(db.String, nullable=False) + score = db.Column(db.Numeric, nullable=True) + + node_1 = db.relationship( + 'Node', backref=orm.backref('edges_primary', uselist=True, lazy='noload'), + uselist=False, lazy='noload', primaryjoin='Node.id==Edge.node_1_id') + + node_2 = db.relationship( + 'Node', backref=orm.backref('edges_secondary', uselist=True, lazy='noload'), + uselist=False, lazy='noload', primaryjoin='Node.id==Edge.node_2_id') + + def __repr__(self): + return '' % self.id diff --git a/apps/iatlas/api-gitlab/api/db_models/feature.py b/apps/iatlas/api-gitlab/api/db_models/feature.py new file mode 100644 index 0000000000..bb10608dc3 --- /dev/null +++ b/apps/iatlas/api-gitlab/api/db_models/feature.py @@ -0,0 +1,23 @@ +from sqlalchemy import orm +from api import db +from . import Base +from api.enums import unit_enum + + +class Feature(Base): + __tablename__ = 'features' + id = db.Column(db.String, primary_key=True) + name = db.Column(db.String, nullable=False) + display = db.Column(db.String, nullable=True) + order = db.Column(db.Integer, nullable=True) + unit = db.Column(unit_enum, nullable=True) + germline_category = db.Column(db.String, nullable=True) + germline_module = db.Column(db.String, nullable=True) + feature_class = db.Column(db.String, nullable=False) + method_tag = db.Column(db.String, nullable=False) + + samples = db.relationship( + "Sample", secondary='features_to_samples', uselist=True, lazy='noload') + + def __repr__(self): + return '' % self.name diff --git a/apps/iatlas/api-gitlab/api/db_models/feature_to_sample.py b/apps/iatlas/api-gitlab/api/db_models/feature_to_sample.py new file mode 100644 index 0000000000..35b5f33d4f --- /dev/null +++ b/apps/iatlas/api-gitlab/api/db_models/feature_to_sample.py @@ -0,0 +1,25 @@ +from sqlalchemy import orm +from api import db +from . import Base + + +class FeatureToSample(Base): + __tablename__ = 'features_to_samples' + + id = db.Column(db.String, primary_key=True) + feature_to_sample_value = db.Column(db.Numeric, nullable=True) + + feature_id = db.Column(db.String, db.ForeignKey( + 'features.id'), primary_key=True) + + sample_id = db.Column(db.String, db.ForeignKey( + 'samples.id'), primary_key=True) + + features = db.relationship('Feature', backref=orm.backref( + 'feature_sample_assoc', uselist=True, lazy='noload'), uselist=True, lazy='noload') + + samples = db.relationship('Sample', backref=orm.backref( + 'feature_sample_assoc', uselist=True, lazy='noload'), uselist=True, lazy='noload') + + def __repr__(self): + return '' % self.feature_id diff --git a/apps/iatlas/api-gitlab/api/db_models/gene.py b/apps/iatlas/api-gitlab/api/db_models/gene.py new file mode 100644 index 0000000000..37de221543 --- /dev/null +++ b/apps/iatlas/api-gitlab/api/db_models/gene.py @@ -0,0 +1,31 @@ +from sqlalchemy import orm +from api import db +from . import Base + + +class Gene(Base): + __tablename__ = 'genes' + id = db.Column(db.String, primary_key=True) + entrez_id = db.Column(db.Integer, nullable=False) + hgnc_id = db.Column(db.String, nullable=False) + description = db.Column(db.String, nullable=True) + friendly_name = db.Column(db.String, nullable=True) + io_landscape_name = db.Column(db.String, nullable=True) + gene_family = db.Column(db.String, nullable=True) + gene_function = db.Column(db.String, nullable=True) + immune_checkpoint = db.Column(db.String, nullable=True) + gene_pathway = db.Column(db.String, nullable=True) + super_category = db.Column(db.String, nullable=True) + therapy_type = db.Column(db.String, nullable=True) + + gene_sets = db.relationship( + "GeneSet", secondary='genes_to_gene_sets', uselist=True, lazy='noload') + + publications = db.relationship( + "Publication", secondary='publications_to_genes_to_gene_sets', uselist=True, lazy='noload') + + samples = db.relationship( + "Sample", secondary='genes_to_samples', uselist=True, lazy='noload') + + def __repr__(self): + return '' % self.entrez_id diff --git a/apps/iatlas/api-gitlab/api/db_models/gene_set.py b/apps/iatlas/api-gitlab/api/db_models/gene_set.py new file mode 100644 index 0000000000..58a843d4af --- /dev/null +++ b/apps/iatlas/api-gitlab/api/db_models/gene_set.py @@ -0,0 +1,18 @@ +from api import db +from . import Base + + +class GeneSet(Base): + __tablename__ = 'gene_sets' + id = db.Column(db.String, primary_key=True) + name = db.Column(db.String, nullable=False) + display = db.Column(db.String, nullable=True) + + genes = db.relationship( + 'Gene', secondary='genes_to_gene_sets', uselist=True, lazy='noload') + + publications = db.relationship( + 'Publication', secondary='publications_to_genes_to_gene_sets', uselist=True, lazy='noload') + + def __repr__(self): + return '' % self.name diff --git a/apps/iatlas/api-gitlab/api/db_models/gene_to_gene_set.py b/apps/iatlas/api-gitlab/api/db_models/gene_to_gene_set.py new file mode 100644 index 0000000000..fdc0bb1895 --- /dev/null +++ b/apps/iatlas/api-gitlab/api/db_models/gene_to_gene_set.py @@ -0,0 +1,23 @@ +from sqlalchemy import orm +from api import db +from . import Base + + +class GeneToGeneSet(Base): + __tablename__ = 'genes_to_gene_sets' + id = db.Column(db.String, primary_key=True) + + gene_id = db.Column( + db.String, db.ForeignKey('genes.id'), primary_key=True) + + gene_set_id = db.Column( + db.String, db.ForeignKey('gene_sets.id'), primary_key=True) + + genes = db.relationship('Gene', backref=orm.backref( + 'gene_set_assoc', uselist=True, lazy='noload'), uselist=True, lazy='noload') + + gene_sets = db.relationship('GeneSet', backref=orm.backref( + 'gene_set_assoc', uselist=True, lazy='noload'), uselist=True, lazy='noload') + + def __repr__(self): + return '' % self.gene_id diff --git a/apps/iatlas/api-gitlab/api/db_models/gene_to_sample.py b/apps/iatlas/api-gitlab/api/db_models/gene_to_sample.py new file mode 100644 index 0000000000..fcf8ab300c --- /dev/null +++ b/apps/iatlas/api-gitlab/api/db_models/gene_to_sample.py @@ -0,0 +1,26 @@ +from sqlalchemy import orm +from api import db +from . import Base + + +class GeneToSample(Base): + __tablename__ = 'genes_to_samples' + + gene_id = db.Column(db.String, db.ForeignKey( + 'genes.id'), primary_key=True) + + sample_id = db.Column(db.String, db.ForeignKey( + 'samples.id'), primary_key=True) + + rna_seq_expression = db.Column(db.Numeric, nullable=True) + + nanostring_expression = db.Column(db.Numeric, nullable=True) + + gene = db.relationship('Gene', backref=orm.backref( + 'gene_sample_assoc', uselist=True, lazy='noload'), uselist=False, lazy='noload') + + sample = db.relationship('Sample', backref=orm.backref( + 'gene_sample_assoc', uselist=True, lazy='noload'), uselist=False, lazy='noload') + + def __repr__(self): + return '' % self.gene_id diff --git a/apps/iatlas/api-gitlab/api/db_models/germline_gwas_result.py b/apps/iatlas/api-gitlab/api/db_models/germline_gwas_result.py new file mode 100644 index 0000000000..44b76a81c7 --- /dev/null +++ b/apps/iatlas/api-gitlab/api/db_models/germline_gwas_result.py @@ -0,0 +1,34 @@ +from sqlalchemy import orm +from api import db +from . import Base + + +class GermlineGwasResult(Base): + __tablename__ = 'germline_gwas_results' + id = db.Column(db.String, primary_key=True) + p_value = db.Column(db.Numeric, nullable=True) + maf = db.Column(db.Numeric, nullable=True) + + dataset_id = db.Column(db.String, db.ForeignKey( + 'datasets.id'), nullable=False) + + feature_id = db.Column(db.String, db.ForeignKey( + 'features.id'), nullable=False) + + snp_id = db.Column(db.String, db.ForeignKey( + 'snps.id'), nullable=False) + + data_set = db.relationship( + 'Dataset', backref=orm.backref('germline_gwas_results', uselist=True, lazy='noload'), + uselist=False, lazy='noload') + + feature = db.relationship( + 'Feature', backref=orm.backref('germline_gwas_results', uselist=True, lazy='noload'), + uselist=False, lazy='noload') + + snp = db.relationship( + 'Snp', backref=orm.backref('germline_gwas_results', uselist=True, lazy='noload'), + uselist=False, lazy='noload') + + def __repr__(self): + return '' % self.id diff --git a/apps/iatlas/api-gitlab/api/db_models/heritability_result.py b/apps/iatlas/api-gitlab/api/db_models/heritability_result.py new file mode 100644 index 0000000000..553b7e1089 --- /dev/null +++ b/apps/iatlas/api-gitlab/api/db_models/heritability_result.py @@ -0,0 +1,30 @@ +from sqlalchemy import orm +from api import db +from . import Base + + +class HeritabilityResult(Base): + __tablename__ = 'heritability_results' + id = db.Column(db.String, primary_key=True) + p_value = db.Column(db.Numeric, nullable=True) + fdr = db.Column(db.Numeric, nullable=True) + variance = db.Column(db.Numeric, nullable=True) + se = db.Column(db.Numeric, nullable=True) + cluster = db.Column(db.String, nullable=False) + + dataset_id = db.Column(db.String, db.ForeignKey( + 'datasets.id'), nullable=False) + + feature_id = db.Column(db.String, db.ForeignKey( + 'features.id'), nullable=False) + + data_set = db.relationship( + 'Dataset', backref=orm.backref('heritability_results', uselist=True, lazy='noload'), + uselist=False, lazy='noload') + + feature = db.relationship( + 'Feature', backref=orm.backref('heritability_results', uselist=True, lazy='noload'), + uselist=False, lazy='noload') + + def __repr__(self): + return '' % self.id diff --git a/apps/iatlas/api-gitlab/api/db_models/mutation.py b/apps/iatlas/api-gitlab/api/db_models/mutation.py new file mode 100644 index 0000000000..67b99e5762 --- /dev/null +++ b/apps/iatlas/api-gitlab/api/db_models/mutation.py @@ -0,0 +1,28 @@ +from sqlalchemy import orm +from api import db +from . import Base + +class Mutation(Base): + __tablename__ = 'mutations' + id = db.Column(db.String, primary_key=True) + name = db.Column(db.String, nullable=False) + mutation_code = db.Column(db.String, nullable=False) + + gene_id = db.Column(db.String, db.ForeignKey('genes.id'), nullable=False) + + mutation_type_id = db.Column( + db.String, db.ForeignKey('mutation_types.id'), nullable=True) + + gene = db.relationship( + "Gene", backref=orm.backref('mutations', uselist=True, lazy='noload'), + uselist=False, lazy='noload') + + mutation_type = db.relationship( + "MutationType", backref=orm.backref('mutations', uselist=True, lazy='noload'), + uselist=False, lazy='noload') + + samples = db.relationship( + "Sample", secondary='samples_to_mutations', uselist=True, lazy='noload') + + def __repr__(self): + return '' % self.id diff --git a/apps/iatlas/api-gitlab/api/db_models/mutation_type.py b/apps/iatlas/api-gitlab/api/db_models/mutation_type.py new file mode 100644 index 0000000000..5d823e01e4 --- /dev/null +++ b/apps/iatlas/api-gitlab/api/db_models/mutation_type.py @@ -0,0 +1,12 @@ +from api import db +from . import Base + + +class MutationType(Base): + __tablename__ = 'mutation_types' + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String, nullable=False) + display = db.Column(db.String, nullable=True) + + def __repr__(self): + return '' % self.name diff --git a/apps/iatlas/api-gitlab/api/db_models/neoantigen.py b/apps/iatlas/api-gitlab/api/db_models/neoantigen.py new file mode 100644 index 0000000000..6a687cf92d --- /dev/null +++ b/apps/iatlas/api-gitlab/api/db_models/neoantigen.py @@ -0,0 +1,32 @@ +from sqlalchemy import orm +from api import db +from . import Base + + +class Neoantigen(Base): + __tablename__ = 'neoantigens' + id = db.Column(db.String, primary_key=True) + tpm = db.Column(db.Float, nullable=True) + pmhc = db.Column(db.String, nullable=False) + freq_pmhc = db.Column(db.Integer, nullable=False) + patient_id = db.Column(db.String, db.ForeignKey( + 'patients.id'), nullable=False) + neoantigen_gene_id = db.Column( + db.String, db.ForeignKey('genes.id'), nullable=True) + + gene = db.relationship( + 'Gene', + backref=orm.backref('neoantigen_assoc', uselist=True, lazy='noload'), + uselist=True, + lazy='noload' + ) + + patient = db.relationship( + 'Patient', + backref=orm.backref('neoantigen_assoc', uselist=True, lazy='noload'), + uselist=True, + lazy='noload' + ) + + def __repr__(self): + return '' % self.id diff --git a/apps/iatlas/api-gitlab/api/db_models/node.py b/apps/iatlas/api-gitlab/api/db_models/node.py new file mode 100644 index 0000000000..f1945b69f7 --- /dev/null +++ b/apps/iatlas/api-gitlab/api/db_models/node.py @@ -0,0 +1,62 @@ +from sqlalchemy import orm +from api import db +from . import Base + + +class Node(Base): + __tablename__ = 'nodes' + id = db.Column(db.String, primary_key=True) + label = db.Column(db.String, nullable=True) + network = db.Column(db.String, nullable=False) + name = db.Column(db.String, nullable=False) + score = db.Column(db.Numeric, nullable=True) + x = db.Column(db.Numeric, nullable=True) + y = db.Column(db.Numeric, nullable=True) + + dataset_id = db.Column( + db.String, db.ForeignKey('datasets.id'), nullable=True) + + node_feature_id = db.Column( + db.String, db.ForeignKey('features.id'), nullable=True) + + node_gene_id = db.Column(db.String, db.ForeignKey('genes.id'), nullable=True) + + tag_1_id = db.Column( + db.String, db.ForeignKey('tags.id'), nullable=False) + + tag_2_id = db.Column( + db.String, db.ForeignKey('tags.id'), nullable=True) + + + data_set = db.relationship( + 'Dataset', backref=orm.backref('node', uselist=True, lazy='noload'), + uselist=False, lazy='noload') + + feature = db.relationship( + 'Feature', backref=orm.backref('node', uselist=True, lazy='noload'), + uselist=False, lazy='noload') + + gene = db.relationship( + 'Gene', backref=orm.backref('node', uselist=True, lazy='noload'), + uselist=False, lazy='noload') + + tag1 = db.relationship( + 'Tag', + backref=orm.backref('node1', uselist=True, lazy='noload'), + uselist=False, + lazy='noload', + foreign_keys=tag_1_id + ) + + tag2 = db.relationship( + 'Tag', + backref=orm.backref('node2', uselist=True, lazy='noload'), + uselist=False, + lazy='noload', + foreign_keys=tag_2_id + ) + + + + def __repr__(self): + return '' % self.id diff --git a/apps/iatlas/api-gitlab/api/db_models/patient.py b/apps/iatlas/api-gitlab/api/db_models/patient.py new file mode 100644 index 0000000000..4832b359a0 --- /dev/null +++ b/apps/iatlas/api-gitlab/api/db_models/patient.py @@ -0,0 +1,19 @@ +from sqlalchemy import orm +from api import db +from . import Base +from api.enums import ethnicity_enum, gender_enum, race_enum + + +class Patient(Base): + __tablename__ = 'patients' + id = db.Column(db.String, primary_key=True) + age_at_diagnosis = db.Column(db.Integer, nullable=True) + name = db.Column(db.String, nullable=False) + ethnicity = db.Column(ethnicity_enum, nullable=True) + gender = db.Column(gender_enum, nullable=True) + height = db.Column(db.Integer, nullable=True) + race = db.Column(race_enum, nullable=True) + weight = db.Column(db.Integer, nullable=True) + + def __repr__(self): + return '' % self.name diff --git a/apps/iatlas/api-gitlab/api/db_models/publication.py b/apps/iatlas/api-gitlab/api/db_models/publication.py new file mode 100644 index 0000000000..1c55277a4b --- /dev/null +++ b/apps/iatlas/api-gitlab/api/db_models/publication.py @@ -0,0 +1,26 @@ +from api import db +from . import Base + + +class Publication(Base): + __tablename__ = 'publications' + id = db.Column(db.String, primary_key=True) + do_id = db.Column(db.String, nullable=True) + first_author_last_name = db.Column(db.String, nullable=True) + journal = db.Column(db.String, nullable=True) + link = db.Column(db.String, nullable=False) + pubmed_id = db.Column(db.Integer, nullable=True) + title = db.Column(db.String, nullable=True) + year = db.Column(db.Integer, nullable=True) + + genes = db.relationship( + 'Gene', secondary='publications_to_genes_to_gene_sets', uselist=True, lazy='noload') + + gene_sets = db.relationship( + 'GeneSet', secondary='publications_to_genes_to_gene_sets', uselist=True, lazy='noload') + + tags = db.relationship( + 'Tag', secondary='tags_to_publications', uselist=True, lazy='noload') + + def __repr__(self): + return '' % self.title diff --git a/apps/iatlas/api-gitlab/api/db_models/publication_to_gene_to_gene_set.py b/apps/iatlas/api-gitlab/api/db_models/publication_to_gene_to_gene_set.py new file mode 100644 index 0000000000..35aa92b6e9 --- /dev/null +++ b/apps/iatlas/api-gitlab/api/db_models/publication_to_gene_to_gene_set.py @@ -0,0 +1,28 @@ +from sqlalchemy import orm +from api import db +from . import Base + + +class PublicationToGeneToGeneSet(Base): + __tablename__ = 'publications_to_genes_to_gene_sets' + + gene_id = db.Column( + db.String, db.ForeignKey('genes.id'), primary_key=True) + + gene_set_id = db.Column( + db.String, db.ForeignKey('gene_sets.id'), primary_key=True) + + publication_id = db.Column( + db.String, db.ForeignKey('publications.id'), primary_key=True) + + genes = db.relationship('Gene', backref=orm.backref( + 'publication_gene_gene_set_assoc', uselist=True, lazy='noload'), uselist=True, lazy='noload') + + gene_sets = db.relationship('GeneSet', backref=orm.backref( + 'publication_gene_gene_set_assoc', uselist=True, lazy='noload'), uselist=True, lazy='noload') + + publications = db.relationship('Publication', backref=orm.backref( + 'publication_gene_gene_set_assoc', uselist=True, lazy='noload'), uselist=True, lazy='noload') + + def __repr__(self): + return '' % self.gene_id diff --git a/apps/iatlas/api-gitlab/api/db_models/rare_variant_pathway_associations.py b/apps/iatlas/api-gitlab/api/db_models/rare_variant_pathway_associations.py new file mode 100644 index 0000000000..88d9dbdba0 --- /dev/null +++ b/apps/iatlas/api-gitlab/api/db_models/rare_variant_pathway_associations.py @@ -0,0 +1,35 @@ +from sqlalchemy import orm +from api import db +from . import Base + + +class RareVariantPathwayAssociation(Base): + __tablename__ = 'rare_variant_pathway_associations' + id = db.Column(db.String, primary_key=True) + pathway = db.Column(db.String) + p_value = db.Column(db.Numeric, nullable=True) + min = db.Column(db.Numeric, nullable=True) + max = db.Column(db.Numeric, nullable=True) + mean = db.Column(db.Numeric, nullable=True) + q1 = db.Column(db.Numeric, nullable=True) + q2 = db.Column(db.Numeric, nullable=True) + q3 = db.Column(db.Numeric, nullable=True) + n_mutants = db.Column(db.Integer, nullable=True) + n_total = db.Column(db.Integer, nullable=True) + + dataset_id = db.Column(db.String, db.ForeignKey( + 'datasets.id'), nullable=False) + + feature_id = db.Column(db.String, db.ForeignKey( + 'features.id'), nullable=False) + + data_set = db.relationship( + 'Dataset', backref=orm.backref('rare_variant_pathway_associations', uselist=True, lazy='noload'), + uselist=False, lazy='noload') + + feature = db.relationship( + 'Feature', backref=orm.backref('rare_variant_pathway_associations', uselist=True, lazy='noload'), + uselist=False, lazy='noload') + + def __repr__(self): + return '' % self.id diff --git a/apps/iatlas/api-gitlab/api/db_models/sample.py b/apps/iatlas/api-gitlab/api/db_models/sample.py new file mode 100644 index 0000000000..4ce9bd4e31 --- /dev/null +++ b/apps/iatlas/api-gitlab/api/db_models/sample.py @@ -0,0 +1,34 @@ +from sqlalchemy import orm +from api import db +from . import Base + + +class Sample(Base): + __tablename__ = 'samples' + id = db.Column(db.String, primary_key=True) + name = db.Column(db.String, nullable=False) + + patient_id = db.Column( + db.String, db.ForeignKey('patients.id'), nullable=True) + + data_sets = db.relationship( + "Dataset", secondary='datasets_to_samples', uselist=True, lazy='noload') + + features = db.relationship( + "Feature", secondary='features_to_samples', uselist=True, lazy='noload') + + genes = db.relationship( + "Gene", secondary='genes_to_samples', uselist=True, lazy='noload') + + mutations = db.relationship( + "Mutation", secondary='samples_to_mutations', uselist=True, lazy='noload') + + tags = db.relationship( + "Tag", secondary='samples_to_tags', uselist=True, lazy='noload') + + patient = db.relationship( + "Patient", backref=orm.backref('samples', uselist=True, lazy='noload'), + uselist=False, lazy='noload') + + def __repr__(self): + return '' % self.name diff --git a/apps/iatlas/api-gitlab/api/db_models/sample_to_mutation.py b/apps/iatlas/api-gitlab/api/db_models/sample_to_mutation.py new file mode 100644 index 0000000000..bd90eba3e3 --- /dev/null +++ b/apps/iatlas/api-gitlab/api/db_models/sample_to_mutation.py @@ -0,0 +1,25 @@ +from sqlalchemy import orm +from api import db +from . import Base +from api.enums import status_enum + + +class SampleToMutation(Base): + __tablename__ = 'samples_to_mutations' + id = db.Column(db.String, primary_key=True) + mutation_status = db.Column(status_enum, nullable=False) + + sample_id = db.Column( + db.String, db.ForeignKey('samples.id'), primary_key=True) + + mutation_id = db.Column( + db.String, db.ForeignKey('mutations.id'), primary_key=True) + + samples = db.relationship('Sample', backref=orm.backref( + 'sample_mutation_assoc', uselist=True, lazy='noload'), uselist=True, lazy='noload') + + mutations = db.relationship('Mutation', backref=orm.backref( + 'sample_mutation_assoc', uselist=True, lazy='noload'), uselist=True, lazy='noload') + + def __repr__(self): + return '' % self.sample_id diff --git a/apps/iatlas/api-gitlab/api/db_models/sample_to_tag.py b/apps/iatlas/api-gitlab/api/db_models/sample_to_tag.py new file mode 100644 index 0000000000..86f569a84a --- /dev/null +++ b/apps/iatlas/api-gitlab/api/db_models/sample_to_tag.py @@ -0,0 +1,22 @@ +from sqlalchemy import orm +from api import db +from . import Base + + +class SampleToTag(Base): + __tablename__ = 'samples_to_tags' + + sample_id = db.Column( + db.String, db.ForeignKey('samples.id'), primary_key=True) + + tag_id = db.Column( + db.String, db.ForeignKey('tags.id'), primary_key=True) + + samples = db.relationship('Sample', backref=orm.backref( + 'sample_tag_assoc', uselist=True, lazy='noload'), uselist=True, lazy='noload') + + tags = db.relationship('Tag', backref=orm.backref( + 'sample_tag_assoc', uselist=True, lazy='noload'), uselist=True, lazy='noload') + + def __repr__(self): + return '' % self.sample_id diff --git a/apps/iatlas/api-gitlab/api/db_models/single_cell_pseudobulk.py b/apps/iatlas/api-gitlab/api/db_models/single_cell_pseudobulk.py new file mode 100644 index 0000000000..123677f22f --- /dev/null +++ b/apps/iatlas/api-gitlab/api/db_models/single_cell_pseudobulk.py @@ -0,0 +1,25 @@ +from sqlalchemy import orm +from api import db +from . import Base + + +class SingleCellPseudobulk(Base): + __tablename__ = 'single_cell_pseudobulk' + id = db.Column(db.String, primary_key=True) + cell_type = db.Column(db.String, nullable=False) + single_cell_seq_sum = db.Column(db.Numeric, nullable=False) + + gene_id = db.Column(db.String, db.ForeignKey( + 'genes.id'), primary_key=True) + + sample_id = db.Column(db.String, db.ForeignKey( + 'samples.id'), primary_key=True) + + gene = db.relationship('Gene', backref=orm.backref( + 'pseudobulk_assoc', uselist=True, lazy='noload'), uselist=False, lazy='noload') + + sample = db.relationship('Sample', backref=orm.backref( + 'pseudobulk_assoc', uselist=True, lazy='noload'), uselist=False, lazy='noload') + + def __repr__(self): + return '' % self.gene_id \ No newline at end of file diff --git a/apps/iatlas/api-gitlab/api/db_models/single_cell_pseudobulk_feature.py b/apps/iatlas/api-gitlab/api/db_models/single_cell_pseudobulk_feature.py new file mode 100644 index 0000000000..cdd1b5a119 --- /dev/null +++ b/apps/iatlas/api-gitlab/api/db_models/single_cell_pseudobulk_feature.py @@ -0,0 +1,25 @@ +from sqlalchemy import orm +from api import db +from . import Base + + +class SingleCellPseudobulkFeature(Base): + __tablename__ = 'single_cell_pseudobulk_features' + id = db.Column(db.String, primary_key=True) + cell_type = db.Column(db.String, nullable=False) + value = db.Column(db.Numeric, nullable=False) + + feature_id = db.Column(db.String, db.ForeignKey( + 'features.id'), primary_key=True) + + sample_id = db.Column(db.String, db.ForeignKey( + 'samples.id'), primary_key=True) + + feature = db.relationship('Feature', backref=orm.backref( + 'pseudobulk_feature_assoc', uselist=True, lazy='noload'), uselist=False, lazy='noload') + + sample = db.relationship('Sample', backref=orm.backref( + 'pseudobulk_feature_assoc', uselist=True, lazy='noload'), uselist=False, lazy='noload') + + def __repr__(self): + return '' % self.feature_id \ No newline at end of file diff --git a/apps/iatlas/api-gitlab/api/db_models/slide.py b/apps/iatlas/api-gitlab/api/db_models/slide.py new file mode 100644 index 0000000000..d023b2e239 --- /dev/null +++ b/apps/iatlas/api-gitlab/api/db_models/slide.py @@ -0,0 +1,20 @@ +from sqlalchemy import orm +from api import db +from . import Base + + +class Slide(Base): + __tablename__ = 'slides' + id = db.Column(db.String, primary_key=True) + name = db.Column(db.String, nullable=False) + description = db.Column(db.String, nullable=True) + + patient_id = db.Column( + db.String, db.ForeignKey('patients.id'), nullable=True) + + patient = db.relationship( + 'Patient', backref=orm.backref('slides', uselist=True, lazy='noload'), + uselist=False, lazy='noload') + + def __repr__(self): + return '' % self.name diff --git a/apps/iatlas/api-gitlab/api/db_models/snp.py b/apps/iatlas/api-gitlab/api/db_models/snp.py new file mode 100644 index 0000000000..65a0dd8795 --- /dev/null +++ b/apps/iatlas/api-gitlab/api/db_models/snp.py @@ -0,0 +1,15 @@ +from sqlalchemy import orm +from api import db +from . import Base + + +class Snp(Base): + __tablename__ = 'snps' + id = db.Column(db.String, primary_key=True) + name = db.Column(db.String, nullable=False) + rsid = db.Column(db.String, nullable=True) + chr = db.Column(db.String, nullable=True) + bp = db.Column(db.Integer, nullable=True) + + def __repr__(self): + return '' % self.name diff --git a/apps/iatlas/api-gitlab/api/db_models/tag.py b/apps/iatlas/api-gitlab/api/db_models/tag.py new file mode 100644 index 0000000000..c1be189698 --- /dev/null +++ b/apps/iatlas/api-gitlab/api/db_models/tag.py @@ -0,0 +1,35 @@ +from api import db +from . import Base +from api.enums import tag_type_enum + + +class Tag(Base): + __tablename__ = 'tags' + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String, nullable=False) + description = db.Column(db.String, nullable=True) + color = db.Column(db.String, nullable=True) + long_display = db.Column(db.String, nullable=True) + short_display = db.Column(db.String, nullable=True) + tag_type = db.Column(tag_type_enum, nullable=False) + order = db.Column(db.Integer, nullable=True) + + data_sets = db.relationship( + 'Dataset', lazy='noload', uselist=True, secondary='datasets_to_tags') + + publications = db.relationship( + 'Publication', lazy='noload', uselist=True, secondary='tags_to_publications') + + related_tags = db.relationship( + 'Tag', foreign_keys='TagToTag.tag_id', lazy='noload', + secondary='tags_to_tags', back_populates='tags', uselist=True) + + samples = db.relationship( + 'Sample', lazy='noload', uselist=True, secondary='samples_to_tags') + + tags = db.relationship( + 'Tag', foreign_keys='TagToTag.related_tag_id', lazy='noload', + secondary='tags_to_tags', back_populates='related_tags', uselist=True) + + def __repr__(self): + return '' % self.name diff --git a/apps/iatlas/api-gitlab/api/db_models/tag_to_publication.py b/apps/iatlas/api-gitlab/api/db_models/tag_to_publication.py new file mode 100644 index 0000000000..71bd33d2fe --- /dev/null +++ b/apps/iatlas/api-gitlab/api/db_models/tag_to_publication.py @@ -0,0 +1,22 @@ +from sqlalchemy import orm +from api import db +from . import Base + + +class TagToPublication(Base): + __tablename__ = 'tags_to_publications' + + publication_id = db.Column( + db.String, db.ForeignKey('publications.id'), primary_key=True) + + tag_id = db.Column( + db.String, db.ForeignKey('tags.id'), primary_key=True) + + publications = db.relationship('Publication', backref=orm.backref( + 'tag_publication_assoc', uselist=True, lazy='noload'), uselist=True, lazy='noload') + + tags = db.relationship('Tag', backref=orm.backref( + 'tag_publication_assoc', uselist=True, lazy='noload'), uselist=True, lazy='noload') + + def __repr__(self): + return '' % self.tag_id diff --git a/apps/iatlas/api-gitlab/api/db_models/tag_to_tag.py b/apps/iatlas/api-gitlab/api/db_models/tag_to_tag.py new file mode 100644 index 0000000000..b30172f52e --- /dev/null +++ b/apps/iatlas/api-gitlab/api/db_models/tag_to_tag.py @@ -0,0 +1,24 @@ +from sqlalchemy import orm +from api import db +from . import Base + + +class TagToTag(Base): + __tablename__ = 'tags_to_tags' + + tag_id = db.Column( + db.String, db.ForeignKey('tags.id'), primary_key=True) + + related_tag_id = db.Column( + db.String, db.ForeignKey('tags.id'), primary_key=True) + + tags = db.relationship( + 'Tag', backref=orm.backref('tag_related_assoc', uselist=True, lazy='noload'), + uselist=True, primaryjoin='Tag.id==TagToTag.tag_id', lazy='noload') + + related_tags = db.relationship( + 'Tag', backref=orm.backref('related_tag_assoc', uselist=True, lazy='noload'), + uselist=True, primaryjoin='Tag.id==TagToTag.related_tag_id', lazy='noload') + + def __repr__(self): + return '' % self.tag_id diff --git a/apps/iatlas/api-gitlab/api/enums.py b/apps/iatlas/api-gitlab/api/enums.py new file mode 100644 index 0000000000..a30cb5bf33 --- /dev/null +++ b/apps/iatlas/api-gitlab/api/enums.py @@ -0,0 +1,24 @@ +from sqlalchemy.dialects.postgresql import ENUM + +direction_enum = ENUM('Amp', 'Del', name='direction_enum') + +ethnicity_enum = ENUM('not hispanic or latino', + 'hispanic or latino', name='ethnicity_enum') + +gender_enum = ENUM('male', 'female', name='gender_enum') + +race_enum = ENUM('white', 'asian', 'american indian or alaska native', + 'black or african american', 'native hawaiian or other pacific islander', name='race_enum') + +status_enum = ENUM('Wt', 'Mut', name='status_enum') + +unit_enum = ENUM('Count', 'Fraction', 'Per Megabase', + 'Score', 'Year', name='unit_enum') + +qtl_enum = ENUM('sQTL', 'eQTL') + +ecaviar_pp_enum = ENUM('C1', 'C2') + +coloc_plot_type_enum = ENUM('3 Level Plot', 'Expanded Region') + +tag_type_enum = ENUM('group', 'parent_group', 'meta_group', 'network') diff --git a/apps/iatlas/api-gitlab/api/extensions.py b/apps/iatlas/api-gitlab/api/extensions.py new file mode 100644 index 0000000000..ddd813820b --- /dev/null +++ b/apps/iatlas/api-gitlab/api/extensions.py @@ -0,0 +1,5 @@ +from flask_sqlalchemy import SQLAlchemy +from api.logger import LogSetup + +db = SQLAlchemy() +logs = LogSetup() diff --git a/apps/iatlas/api-gitlab/api/logger/LOGGING.md b/apps/iatlas/api-gitlab/api/logger/LOGGING.md new file mode 100644 index 0000000000..39c12b49df --- /dev/null +++ b/apps/iatlas/api-gitlab/api/logger/LOGGING.md @@ -0,0 +1,31 @@ +# iAtlas API Logging + +[BACK TO MAIN README](./../../README.md) + +Logging is a great way to capture application information. + +Logs can be captured at various levels: + +- DEBUG +- WARN +- INFO +- ERROR + +The application initializes logging when it is created (see `create_app` in [`api/__init__.py`](api/__init__.py)). The formatting is determined by values in the config (see [`config.py`](./../../config.py)). + +The `development` environment is set at log level DEBUG, the `staging` environment is set at log level INFO, and the `production` environment is set to log level WARN. Tests are set to log level INFO. + +To use logging, import logging, get the logger you want to use, and create a log of the appropriate level: + +```python +import logging + +logger = logging.getLogger('logger name here') + +logger.debug('This is a debugging log') +logger.warn('This is a warning log') +logger.info('This is an info log') +logger.error('This is an error log') +``` + +Logs are saved to the `.logs/` folder in the root of the project. This folder is NOT versioned in the repository. Older logs are moved to a timestamped log file and newer logs are saved to the main `iatlas-api.log` file. diff --git a/apps/iatlas/api-gitlab/api/logger/__init__.py b/apps/iatlas/api-gitlab/api/logger/__init__.py new file mode 100644 index 0000000000..ac9b2a4a5b --- /dev/null +++ b/apps/iatlas/api-gitlab/api/logger/__init__.py @@ -0,0 +1,132 @@ +from logging.config import dictConfig +from os import makedirs, path + +""" +We have options in python for stdout (streamhandling) and file logging +File logging has options for a Rotating file based on size or time (daily) +or a watched file, which supports logrotate style rotation +Most of the changes happen in the handlers, lets define a few standards +Borrowed HEAVILY from https://medium.com/tenable-techblog/the-boring-stuff-flask-logging-21c3a5dd0392 +""" + + +class LogSetup(object): + def __init__(self, app=None, **kwargs): + if app is not None: + self.init_app(app, **kwargs) + + def init_app(self, app): + config = app.config + log_type = config['LOG_TYPE'] + logging_level = config['LOG_LEVEL'] + log_extension = '.log' + if log_type != 'stream': + try: + log_directory = config['LOG_DIR'] + app_log_file_name = config['LOG_APP_NAME'] + log_extension + access_log_file_name = config['LOG_WWW_NAME'] + log_extension + makedirs(log_directory, exist_ok=True) + except KeyError as e: + exit(code="{} is a required parameter for log_type '{}'".format( + e, log_type)) + path_sep = path.sep + app_log = path_sep.join([log_directory, app_log_file_name]) + www_log = path_sep.join([log_directory, access_log_file_name]) + + if log_type == 'stream': + logging_policy = 'logging.StreamHandler' + elif log_type == 'watched': + logging_policy = 'logging.handlers.WatchedFileHandler' + else: + log_copies = config['LOG_COPIES'] + logging_policy = 'logging.handlers.TimedRotatingFileHandler' + log_time_interval = config['LOG_TIME_INT'] + log_interval = config['LOG_INTERVAL'] + + std_format = { + 'formatters': { + 'default': { + 'format': '[%(asctime)s.%(msecs)03d] %(levelname)s %(name)s:%(funcName)s: %(message)s', + 'datefmt': '%d/%b/%Y:%H:%M:%S', + }, + 'access': {'format': '%(message)s'}, + } + } + std_logger = { + 'loggers': { + '': {'level': logging_level, 'handlers': ['default'], 'propagate': True}, + 'app.access': { + 'level': logging_level, + 'handlers': ['access_logs'], + 'propagate': False, + }, + 'root': {'level': logging_level, 'handlers': ['default']}, + } + } + if log_type == 'stream': + logging_handler = { + 'handlers': { + 'default': { + 'level': logging_level, + 'formatter': 'default', + 'class': logging_policy, + }, + 'access_logs': { + 'level': logging_level, + 'class': logging_policy, + 'formatter': 'access', + }, + } + } + elif log_type == 'watched': + logging_handler = { + 'handlers': { + 'default': { + 'level': logging_level, + 'class': logging_policy, + 'filename': app_log, + 'formatter': 'default', + 'delay': True, + }, + 'access_logs': { + 'level': logging_level, + 'class': logging_policy, + 'filename': www_log, + 'formatter': 'access', + 'delay': True, + }, + } + } + else: + logging_handler = { + 'handlers': { + 'default': { + 'level': logging_level, + 'class': logging_policy, + 'filename': app_log, + 'backupCount': log_copies, + 'interval': log_interval, + 'formatter': 'default', + 'delay': True, + 'when': log_time_interval + }, + 'access_logs': { + 'level': logging_level, + 'class': logging_policy, + 'filename': www_log, + 'backupCount': log_copies, + 'interval': log_interval, + 'formatter': 'access', + 'delay': True, + 'when': log_time_interval + }, + } + } + + log_config = { + 'version': 1, + 'formatters': std_format['formatters'], + 'loggers': std_logger['loggers'], + 'handlers': logging_handler['handlers'], + } + dictConfig(log_config) diff --git a/apps/iatlas/api-gitlab/api/main/__init__.py b/apps/iatlas/api-gitlab/api/main/__init__.py new file mode 100644 index 0000000000..e26507d919 --- /dev/null +++ b/apps/iatlas/api-gitlab/api/main/__init__.py @@ -0,0 +1,5 @@ +from flask import Blueprint + +bp = Blueprint('main', __name__) + +from api import routes diff --git a/apps/iatlas/api-gitlab/api/resolvers/__init__.py b/apps/iatlas/api-gitlab/api/resolvers/__init__.py new file mode 100644 index 0000000000..0766b2abec --- /dev/null +++ b/apps/iatlas/api-gitlab/api/resolvers/__init__.py @@ -0,0 +1,25 @@ +from .cell_stats_resolver import resolve_cell_stats +from .cells_resolver import resolve_cells +from .cohorts_resolver import resolve_cohorts +from .colocalizations_resolver import resolve_colocalizations +from .copy_number_results_resolver import resolve_copy_number_results +from .data_sets_resolver import resolve_data_sets +from .driver_results_resolver import resolve_driver_results +from .edges_resolver import resolve_edges +from .features_resolver import resolve_features +from .gene_types_resolver import resolve_gene_types +from .genes_resolver import resolve_genes +from .germline_gwas_results_resolver import resolve_germline_gwas_results +from .heritability_results_resolver import resolve_heritability_results +from .mutations_resolver import resolve_mutations +from .mutation_types_resolver import resolve_mutation_types +from .neoantigens_resolver import resolve_neoantigens +from .nodes_resolver import resolve_nodes +from .patient_resolver import resolve_patients +from .rare_variant_pathway_association_resolver import resolve_rare_variant_pathway_associations +from .samples_resolver import resolve_samples +from .slide_resolver import resolve_slides +from .snp_resolver import resolve_snps +from .tags_resolver import resolve_tags +from .test_resolver import resolve_test + diff --git a/apps/iatlas/api-gitlab/api/resolvers/cell_stats_resolver.py b/apps/iatlas/api-gitlab/api/resolvers/cell_stats_resolver.py new file mode 100644 index 0000000000..26099ad94e --- /dev/null +++ b/apps/iatlas/api-gitlab/api/resolvers/cell_stats_resolver.py @@ -0,0 +1,57 @@ +from .resolver_helpers import ( + build_cell_stat_graphql_response, + build_cell_stat_request, + cell_stat_request_fields, + get_requested, + get_selection_set, + simple_data_set_request_fields, + simple_gene_request_fields, +) + +from .resolver_helpers.paging_utils import paginate, Paging, paging_fields + + +def resolve_cell_stats( + _obj, + info, + distinct=False, + paging=None, + entrez=None + ): + # The selection is nested under the 'items' node. + selection_set = get_selection_set(info=info, child_node='items') + requested = get_requested( + selection_set=selection_set, requested_field_mapping=cell_stat_request_fields) + + data_set_requested = get_requested( + selection_set=selection_set, requested_field_mapping=simple_data_set_request_fields, child_node='dataSet') + + gene_requested = get_requested( + selection_set=selection_set, requested_field_mapping=simple_gene_request_fields, child_node='gene') + + if distinct == False: + # Add the id as a cursor if not selecting distinct + requested.add('id') + + paging = paging if paging else Paging.DEFAULT + + query, count_query = build_cell_stat_request( + requested, + data_set_requested, + gene_requested, + distinct=distinct, + paging=paging, + entrez=entrez + ) + + pagination_requested = get_requested(info, paging_fields, 'paging') + res = paginate( + query, + count_query, + paging, + distinct, + build_cell_stat_graphql_response, + pagination_requested + ) + + return(res) diff --git a/apps/iatlas/api-gitlab/api/resolvers/cells_resolver.py b/apps/iatlas/api-gitlab/api/resolvers/cells_resolver.py new file mode 100644 index 0000000000..4ac70fb7b0 --- /dev/null +++ b/apps/iatlas/api-gitlab/api/resolvers/cells_resolver.py @@ -0,0 +1,63 @@ +from .resolver_helpers import ( + build_cell_graphql_response, + build_cell_request, + cell_request_fields, + get_requested, + get_selection_set, + cell_gene_request_fields, + cell_related_feature_request_fields +) + +from .resolver_helpers.paging_utils import paginate, Paging, paging_fields, create_paging + + +def resolve_cells( + _obj, + info, + distinct=False, + paging=None, + cohort=None, + cell=None, + feature=None + ): + + # The selection is nested under the 'items' node. + selection_set = get_selection_set(info=info, child_node='items') + requested = get_requested( + selection_set=selection_set, requested_field_mapping=cell_request_fields) + + feature_requested = get_requested( + selection_set=selection_set, requested_field_mapping=cell_related_feature_request_fields, child_node='features') + + + if distinct == False: + # Add the id as a cursor if not selecting distinct + requested.add('id') + + max_items = 20 if 'features' in requested else 100_000 + paging = create_paging(paging, max_items) + + query, count_query = build_cell_request( + requested, + distinct=distinct, + paging=paging, + cohort=cohort, + cell=cell, + feature=feature + ) + + pagination_requested = get_requested(info, paging_fields, 'paging') + + res = paginate( + query, + count_query, + paging, + distinct, + build_cell_graphql_response( + requested=requested, + feature_requested=feature_requested + ), + pagination_requested + ) + + return(res) \ No newline at end of file diff --git a/apps/iatlas/api-gitlab/api/resolvers/cohorts_resolver.py b/apps/iatlas/api-gitlab/api/resolvers/cohorts_resolver.py new file mode 100644 index 0000000000..5128f9b96c --- /dev/null +++ b/apps/iatlas/api-gitlab/api/resolvers/cohorts_resolver.py @@ -0,0 +1,67 @@ +from .resolver_helpers import build_cohort_graphql_response, build_cohort_request, cohort_request_fields, get_requested, get_selection_set, simple_data_set_request_fields, simple_tag_request_fields, cohort_sample_request_fields, simple_feature_request_fields, simple_gene_request_fields, mutation_request_fields +from .resolver_helpers.paging_utils import paginate, Paging, paging_fields + + +def resolve_cohorts(_obj, info, distinct=False, paging=None, cohort=None, dataSet=None, tag=None): + + selection_set = get_selection_set(info=info, child_node='items') + + requested = get_requested( + selection_set=selection_set, requested_field_mapping=cohort_request_fields) + + data_set_requested = get_requested( + selection_set=selection_set, requested_field_mapping=simple_data_set_request_fields, child_node='dataSet') + + tag_requested = get_requested( + selection_set=selection_set, requested_field_mapping=simple_tag_request_fields, child_node='tag') + + sample_selection_set = get_selection_set( + selection_set=selection_set, child_node='samples') + + sample_requested = get_requested( + selection_set=sample_selection_set, requested_field_mapping=cohort_sample_request_fields) + + sample_tag_selection_set = get_selection_set( + selection_set=sample_selection_set, child_node='tag') + + sample_tag_requested = get_requested( + selection_set=sample_tag_selection_set, requested_field_mapping=simple_tag_request_fields) + + feature_selection_set = get_selection_set( + selection_set=selection_set, child_node='features') + + feature_requested = get_requested( + selection_set=feature_selection_set, requested_field_mapping=simple_feature_request_fields) + + gene_selection_set = get_selection_set( + selection_set=selection_set, child_node='genes') + + gene_requested = get_requested( + selection_set=gene_selection_set, requested_field_mapping=simple_gene_request_fields) + + mutation_selection_set = get_selection_set( + selection_set=selection_set, child_node='mutations') + + mutation_requested = get_requested( + selection_set=mutation_selection_set, requested_field_mapping=mutation_request_fields) + + mutation_gene_selection_set = get_selection_set( + selection_set=mutation_selection_set, child_node='gene') + + mutation_gene_requested = get_requested( + selection_set=mutation_gene_selection_set, requested_field_mapping=simple_gene_request_fields) + + paging = paging if paging else Paging.DEFAULT + + query, count_query = build_cohort_request( + requested, data_set_requested, tag_requested, distinct=distinct, paging=paging, cohort=cohort, data_set=dataSet, tag=tag) + + pagination_requested = get_requested(info, paging_fields, 'paging') + + response_function = build_cohort_graphql_response(requested=requested, sample_requested=sample_requested, sample_tag_requested=sample_tag_requested, + feature_requested=feature_requested, gene_requested=gene_requested, mutation_requested=mutation_requested, mutation_gene_requested=mutation_gene_requested) + + res = paginate(query, count_query, paging, distinct, + response_function, pagination_requested) + + return(res) diff --git a/apps/iatlas/api-gitlab/api/resolvers/colocalizations_resolver.py b/apps/iatlas/api-gitlab/api/resolvers/colocalizations_resolver.py new file mode 100644 index 0000000000..6fbcaed61f --- /dev/null +++ b/apps/iatlas/api-gitlab/api/resolvers/colocalizations_resolver.py @@ -0,0 +1,41 @@ +from .resolver_helpers import build_coloc_graphql_response, build_colocalization_request, colocalization_request_fields, get_requested, get_selection_set, simple_data_set_request_fields, simple_feature_request_fields, simple_gene_request_fields, snp_request_fields + +from .resolver_helpers.paging_utils import paginate, Paging, paging_fields + + +def resolve_colocalizations( + _obj, info, distinct=False, paging=None, dataSet=None, colocDataSet=None, feature=None, entrez=None, snp=None, qtlType=None, eCaviarPP=None, plotType=None): + # The selection is nested under the 'items' node. + selection_set = get_selection_set(info=info, child_node='items') + requested = get_requested( + selection_set=selection_set, requested_field_mapping=colocalization_request_fields) + + data_set_requested = get_requested( + selection_set=selection_set, requested_field_mapping=simple_data_set_request_fields, child_node='dataSet') + + coloc_data_set_requested = get_requested( + selection_set=selection_set, requested_field_mapping=simple_data_set_request_fields, child_node='colocDataSet') + + feature_requested = get_requested( + selection_set=selection_set, requested_field_mapping=simple_feature_request_fields, child_node='feature') + + gene_requested = get_requested( + selection_set=selection_set, requested_field_mapping=simple_gene_request_fields, child_node='gene') + + snp_requested = get_requested( + selection_set=selection_set, requested_field_mapping=snp_request_fields, child_node='snp') + + if distinct == False: + # Add the id as a cursor if not selecting distinct + requested.add('id') + + paging = paging if paging else Paging.DEFAULT + + query, count_query = build_colocalization_request( + requested, data_set_requested, coloc_data_set_requested, feature_requested, gene_requested, snp_requested, distinct=distinct, paging=paging, data_set=dataSet, coloc_data_set=colocDataSet, feature=feature, entrez=entrez, snp=snp, qtl_type=qtlType, ecaviar_pp=eCaviarPP, plot_type=plotType) + + pagination_requested = get_requested(info, paging_fields, 'paging') + res = paginate(query, count_query, paging, distinct, + build_coloc_graphql_response, pagination_requested) + + return(res) diff --git a/apps/iatlas/api-gitlab/api/resolvers/copy_number_results_resolver.py b/apps/iatlas/api-gitlab/api/resolvers/copy_number_results_resolver.py new file mode 100644 index 0000000000..f397e7fea0 --- /dev/null +++ b/apps/iatlas/api-gitlab/api/resolvers/copy_number_results_resolver.py @@ -0,0 +1,36 @@ +from .resolver_helpers import build_cnr_graphql_response, build_copy_number_result_request, cnr_request_fields, feature_request_fields, gene_request_fields, get_requested, get_selection_set, simple_data_set_request_fields, simple_tag_request_fields +from .resolver_helpers.paging_utils import paginate, Paging, paging_fields, create_paging + + +def resolve_copy_number_results(_obj, info, dataSet=None, direction=None, distinct=False, entrez=None, feature=None, maxPValue=None, + maxLog10PValue=None, minLog10PValue=None, minMeanCnv=None, minMeanNormal=None, + minPValue=None, minTStat=None, paging=None, related=None, tag=None): + + # Request fields within 'items' + selection_set = get_selection_set(info=info, child_node='items') + requested = get_requested( + selection_set=selection_set, requested_field_mapping=cnr_request_fields) + + data_set_requested = get_requested( + selection_set=selection_set, requested_field_mapping=simple_data_set_request_fields, child_node='dataSet') + + feature_requested = get_requested( + selection_set=selection_set, requested_field_mapping=feature_request_fields, child_node='feature') + + gene_requested = get_requested( + selection_set=selection_set, requested_field_mapping=gene_request_fields, child_node='gene') + + tag_requested = get_requested( + selection_set=selection_set, requested_field_mapping=simple_tag_request_fields, child_node='tag') + + max_items = 10000 + + paging = create_paging(paging, max_items) + + query, count_query = build_copy_number_result_request( + requested, data_set_requested, feature_requested, gene_requested, tag_requested, + data_set=dataSet, direction=direction, distinct=distinct, entrez=entrez, feature=feature, max_p_value=maxPValue, max_log10_p_value=maxLog10PValue, min_log10_p_value=minLog10PValue, min_mean_cnv=minMeanCnv, min_mean_normal=minMeanNormal, min_p_value=minPValue, min_t_stat=minTStat, paging=paging, related=related, tag=tag) + + # Request fields within 'paging' + pagination_requested = get_requested(info, paging_fields, 'paging') + return paginate(query, count_query, paging, distinct, build_cnr_graphql_response(), pagination_requested) diff --git a/apps/iatlas/api-gitlab/api/resolvers/data_sets_resolver.py b/apps/iatlas/api-gitlab/api/resolvers/data_sets_resolver.py new file mode 100644 index 0000000000..cce07dc8ab --- /dev/null +++ b/apps/iatlas/api-gitlab/api/resolvers/data_sets_resolver.py @@ -0,0 +1,24 @@ +from .resolver_helpers import build_data_set_graphql_response, data_set_request_fields, simple_sample_request_fields, simple_tag_request_fields, get_requested, build_data_set_request, get_selection_set +from .resolver_helpers.paging_utils import paginate, Paging, paging_fields + + +def resolve_data_sets(_obj, info, dataSet=None, sample=None, dataSetType=None, paging=None, distinct=False): + + selection_set = get_selection_set(info=info, child_node='items') + + requested = get_requested( + selection_set=selection_set, requested_field_mapping=data_set_request_fields) + + sample_requested = get_requested( + selection_set=selection_set, requested_field_mapping=simple_sample_request_fields, child_node='samples') + + tag_requested = get_requested( + selection_set=selection_set, requested_field_mapping=simple_tag_request_fields, child_node='tags') + + paging = paging if paging else Paging.DEFAULT + + query, count_query = build_data_set_request( + requested, data_set=dataSet, sample=sample, data_set_type=dataSetType, paging=paging, distinct=distinct) + + pagination_requested = get_requested(info, paging_fields, 'paging') + return paginate(query, count_query, paging, distinct, build_data_set_graphql_response(requested=requested, sample_requested=sample_requested, tag_requested=tag_requested, sample=sample), pagination_requested) diff --git a/apps/iatlas/api-gitlab/api/resolvers/driver_results_resolver.py b/apps/iatlas/api-gitlab/api/resolvers/driver_results_resolver.py new file mode 100644 index 0000000000..40559a44f4 --- /dev/null +++ b/apps/iatlas/api-gitlab/api/resolvers/driver_results_resolver.py @@ -0,0 +1,42 @@ +from .resolver_helpers import build_dr_graphql_response, build_driver_result_request, driver_result_request_fields, get_requested, get_selection_set, simple_data_set_request_fields, simple_feature_request_fields, mutation_request_fields, simple_gene_request_fields, simple_tag_request_fields, mutation_type_request_fields +from .resolver_helpers.paging_utils import paginate, Paging, paging_fields + + +def resolve_driver_results(_obj, info, paging=None, distinct=False, dataSet=None, entrez=None, feature=None, mutation=None, mutationCode=None, related=None, tag=None, maxPValue=None, maxLog10PValue=None, minFoldChange=None, minLog10FoldChange=None, minLog10PValue=None, minPValue=None, minNumMutants=None, minNumWildTypes=None): + + selection_set = get_selection_set(info=info, child_node='items') + + requested = get_requested( + selection_set=selection_set, requested_field_mapping=driver_result_request_fields) + + data_set_requested = get_requested( + selection_set=selection_set, requested_field_mapping=simple_data_set_request_fields, child_node='dataSet') + + feature_requested = get_requested( + selection_set=selection_set, requested_field_mapping=simple_feature_request_fields, child_node='feature') + + mutation_requested = get_requested( + selection_set=selection_set, requested_field_mapping=mutation_request_fields, child_node='mutation') + + mutation_selection_set = get_selection_set( + selection_set=selection_set, child_node='mutation') + + mutation_gene_selection_set = get_selection_set( + selection_set=mutation_selection_set, child_node='gene') + + mutation_gene_requested = get_requested( + selection_set=mutation_gene_selection_set, requested_field_mapping=simple_gene_request_fields) + + mutation_type_requested = get_requested( + selection_set=mutation_selection_set, requested_field_mapping=mutation_type_request_fields, child_node='mutationType') + + tag_requested = get_requested( + selection_set=selection_set, requested_field_mapping=simple_tag_request_fields, child_node='tag') + + paging = paging if paging else Paging.DEFAULT + + query, count_query = build_driver_result_request( + requested, data_set_requested, feature_requested, mutation_requested, mutation_gene_requested, mutation_type_requested, tag_requested, data_set=dataSet, distinct=distinct, entrez=entrez, feature=feature, max_p_value=maxPValue, max_log10_p_value=maxLog10PValue, min_fold_change=minFoldChange, min_log10_fold_change=minLog10FoldChange, min_log10_p_value=minLog10PValue, min_p_value=minPValue, min_n_mut=minNumMutants, min_n_wt=minNumWildTypes, mutation=mutation, mutation_code=mutationCode, paging=paging, related=related, tag=tag + ) + pagination_requested = get_requested(info, paging_fields, 'paging') + return paginate(query, count_query, paging, distinct, build_dr_graphql_response, pagination_requested) diff --git a/apps/iatlas/api-gitlab/api/resolvers/edges_resolver.py b/apps/iatlas/api-gitlab/api/resolvers/edges_resolver.py new file mode 100644 index 0000000000..3e6ea76cf8 --- /dev/null +++ b/apps/iatlas/api-gitlab/api/resolvers/edges_resolver.py @@ -0,0 +1,33 @@ +from .resolver_helpers import (build_edge_graphql_response, build_edge_request, edge_request_fields, + get_requested, get_selection_set, simple_node_request_fields) +from .resolver_helpers.paging_utils import paginate, Paging, paging_fields + + +def resolve_edges(_obj, info, distinct=False, maxScore=None, minScore=None, node1=None, node2=None, paging=None): + ''' + All keyword arguments are optional. Keyword arguments are: + `maxScore` - a float, a maximum score value + `minScore` - a float, a minimum score value + `node1` - a list of strings, starting node names + `node2` - a list of strings, ending node names + ''' + selection_set = get_selection_set(info=info, child_node='items') + requested = get_requested( + selection_set=selection_set, requested_field_mapping=edge_request_fields) + + node_1_requested = get_requested( + selection_set=selection_set, requested_field_mapping=simple_node_request_fields, child_node='node1') + + node_2_requested = get_requested( + selection_set=selection_set, requested_field_mapping=simple_node_request_fields, child_node='node2') + + if distinct == False: + requested.add('id') # Add the id as a cursor if not selecting distinct + + paging = paging if paging else Paging.DEFAULT + + query, count_query = build_edge_request( + requested, node_1_requested, node_2_requested, distinct=distinct, max_score=maxScore, min_score=minScore, node_start=node1, node_end=node2, paging=paging) + + pagination_requested = get_requested(info, paging_fields, 'paging') + return paginate(query, count_query, paging, distinct, build_edge_graphql_response, pagination_requested) diff --git a/apps/iatlas/api-gitlab/api/resolvers/features_resolver.py b/apps/iatlas/api-gitlab/api/resolvers/features_resolver.py new file mode 100644 index 0000000000..fbca046abc --- /dev/null +++ b/apps/iatlas/api-gitlab/api/resolvers/features_resolver.py @@ -0,0 +1,77 @@ +from .resolver_helpers import ( + build_feature_graphql_response, + feature_related_sample_request_fields, + cell_type_feature_related_sample_request_fields, + feature_related_cell_request_fields, + feature_request_fields, + get_requested, + build_features_query, + get_selection_set, + get_requested +) +from .resolver_helpers.paging_utils import paginate, paging_fields, create_paging + + +def resolve_features( + _obj, + info, + distinct=False, + paging=None, + feature=None, + featureClass=None, + maxValue=None, + minValue=None, + sample=None, + cohort=None +): + + selection_set = get_selection_set(info=info, child_node='items') + + requested = get_requested( + selection_set=selection_set, requested_field_mapping=feature_request_fields) + + sample_requested = get_requested( + selection_set=selection_set, requested_field_mapping=feature_related_sample_request_fields, child_node='samples') + + pseudobulk_sample_requested = get_requested( + selection_set=selection_set, requested_field_mapping=cell_type_feature_related_sample_request_fields, child_node='pseudoBulkSamples') + + cell_requested = get_requested( + selection_set=selection_set, requested_field_mapping=feature_related_cell_request_fields, child_node='cells') + + max_items = 10 if 'samples' in requested else 100_000 + + paging = create_paging(paging, max_items) + + query, count_query = build_features_query( + requested, + distinct, + paging, + feature=feature, + feature_class=featureClass, + max_value=maxValue, + min_value=minValue, + sample=sample, + cohort=cohort + ) + + pagination_requested = get_requested(info, paging_fields, 'paging') + + res = paginate( + query, + count_query, + paging, + distinct, + build_feature_graphql_response( + requested = requested, + sample_requested = sample_requested, + pseudobulk_sample_requested = pseudobulk_sample_requested, + cell_requested = cell_requested, + max_value = maxValue, + min_value = minValue, + cohort = cohort, + sample = sample + ), + pagination_requested + ) + return(res) diff --git a/apps/iatlas/api-gitlab/api/resolvers/gene_types_resolver.py b/apps/iatlas/api-gitlab/api/resolvers/gene_types_resolver.py new file mode 100644 index 0000000000..1e9ee750bb --- /dev/null +++ b/apps/iatlas/api-gitlab/api/resolvers/gene_types_resolver.py @@ -0,0 +1,12 @@ +from .resolver_helpers import get_value, request_gene_sets +from .resolver_helpers.gene import build_gene_graphql_response + + +def resolve_gene_types(_obj, info, name=None): + gene_types = request_gene_sets(_obj, info, name=name) + + return [{ + 'display': get_value(gene_type, 'display'), + 'genes': map(build_gene_graphql_response(prefix=""), get_value(gene_type, 'genes', [])), + 'name': get_value(gene_type, 'name') + } for gene_type in gene_types] diff --git a/apps/iatlas/api-gitlab/api/resolvers/genes_resolver.py b/apps/iatlas/api-gitlab/api/resolvers/genes_resolver.py new file mode 100644 index 0000000000..1a192abf6e --- /dev/null +++ b/apps/iatlas/api-gitlab/api/resolvers/genes_resolver.py @@ -0,0 +1,100 @@ +from .resolver_helpers import ( + build_gene_graphql_response, + build_gene_request, + get_selection_set, + gene_related_sample_request_fields, + cell_type_gene_related_sample_request_fields, + gene_related_cell_request_fields, + gene_request_fields, + get_requested, + simple_gene_set_request_fields, + simple_publication_request_fields +) +from .resolver_helpers.paging_utils import paginate, paging_fields, create_paging + + +def resolve_genes( + _obj, + info, + distinct=False, + paging=None, + entrez=None, + geneFamily=None, + geneFunction=None, + geneType=None, + immuneCheckpoint=None, + maxRnaSeqExpr=None, + minRnaSeqExpr=None, + pathway=None, + cohort=None, + sample=None, + superCategory=None, + therapyType=None +): + + selection_set = get_selection_set(info=info, child_node='items') + + requested = get_requested( + selection_set=selection_set, requested_field_mapping=gene_request_fields) + + gene_types_requested = get_requested( + selection_set=selection_set, requested_field_mapping=simple_gene_set_request_fields, child_node='geneTypes') + + publications_requested = get_requested( + selection_set=selection_set, requested_field_mapping=simple_publication_request_fields, child_node='publications') + + samples_requested = get_requested( + selection_set=selection_set, requested_field_mapping=gene_related_sample_request_fields, child_node='samples') + + pseudobulk_sample_requested = get_requested( + selection_set=selection_set, requested_field_mapping=cell_type_gene_related_sample_request_fields, child_node='pseudoBulkSamples') + + cell_requested = get_requested( + selection_set=selection_set, requested_field_mapping=gene_related_cell_request_fields, child_node='cells') + + + max_items = 10 if 'samples' in requested else 100_000 + + paging = create_paging(paging, max_items) + + query, count_query = build_gene_request( + requested, + distinct=distinct, + paging=paging, + entrez=entrez, + gene_family=geneFamily, + gene_function=geneFunction, + gene_type=geneType, + immune_checkpoint=immuneCheckpoint, + max_rna_seq_expr=maxRnaSeqExpr, + min_rna_seq_expr=minRnaSeqExpr, + pathway=pathway, + cohort=cohort, + sample=sample, + super_category=superCategory, + therapy_type=therapyType + ) + + pagination_requested = get_requested(info, paging_fields, 'paging') + + res = paginate( + query, + count_query, + paging, + distinct, + build_gene_graphql_response( + requested, + gene_types_requested, + publications_requested, + samples_requested, + pseudobulk_sample_requested, + cell_requested, + gene_type=geneType, + max_rna_seq_expr=maxRnaSeqExpr, + min_rna_seq_expr=minRnaSeqExpr, + cohort=cohort, + sample=sample + ), + pagination_requested) + + return(res) diff --git a/apps/iatlas/api-gitlab/api/resolvers/germline_gwas_results_resolver.py b/apps/iatlas/api-gitlab/api/resolvers/germline_gwas_results_resolver.py new file mode 100644 index 0000000000..ae1ee2364c --- /dev/null +++ b/apps/iatlas/api-gitlab/api/resolvers/germline_gwas_results_resolver.py @@ -0,0 +1,34 @@ +from .resolver_helpers import build_ggr_graphql_response, build_germline_gwas_result_request, germline_gwas_result_request_fields, get_requested, get_selection_set, simple_data_set_request_fields, simple_feature_request_fields, snp_request_fields + +from .resolver_helpers.paging_utils import paginate, Paging, paging_fields + + +def resolve_germline_gwas_results( + _obj, info, paging=None, distinct=False, dataSet=None, feature=None, snp=None, maxPValue=None, minPValue=None): + + # The selection is nested under the 'items' node. + selection_set = get_selection_set(info=info, child_node='items') + + requested = get_requested( + selection_set=selection_set, requested_field_mapping=germline_gwas_result_request_fields) + + data_set_requested = get_requested( + selection_set=selection_set, requested_field_mapping=simple_data_set_request_fields, child_node='dataSet') + + feature_requested = get_requested( + selection_set=selection_set, requested_field_mapping=simple_feature_request_fields, child_node='feature') + + snp_requested = get_requested( + selection_set=selection_set, requested_field_mapping=snp_request_fields, child_node='snp') + + if distinct == False: + # Add the id as a cursor if not selecting distinct + requested.add('id') + + paging = paging if paging else Paging.DEFAULT + + query, count_query = build_germline_gwas_result_request( + requested, data_set_requested, feature_requested, snp_requested, distinct=distinct, paging=paging, data_set=dataSet, feature=feature, snp=snp, max_p_value=maxPValue, min_p_value=minPValue) + + pagination_requested = get_requested(info, paging_fields, 'paging') + return paginate(query, count_query, paging, distinct, build_ggr_graphql_response, pagination_requested) diff --git a/apps/iatlas/api-gitlab/api/resolvers/heritability_results_resolver.py b/apps/iatlas/api-gitlab/api/resolvers/heritability_results_resolver.py new file mode 100644 index 0000000000..b4dd508bb5 --- /dev/null +++ b/apps/iatlas/api-gitlab/api/resolvers/heritability_results_resolver.py @@ -0,0 +1,25 @@ +from .resolver_helpers import build_hr_graphql_response, build_heritability_result_request, heritability_result_request_fields, get_requested, get_selection_set, simple_data_set_request_fields, simple_feature_request_fields, simple_gene_request_fields, simple_tag_request_fields + +from .resolver_helpers.paging_utils import paginate, Paging, paging_fields + + +def resolve_heritability_results( + _obj, info, dataSet=None, distinct=False, feature=None, maxPValue=None, minFoldChange=None, minPValue=None, paging=None, cluster=None): + # The selection is nested under the 'items' node. + selection_set = get_selection_set(info=info, child_node='items') + requested = get_requested( + selection_set=selection_set, requested_field_mapping=heritability_result_request_fields) + + data_set_requested = get_requested( + selection_set=selection_set, requested_field_mapping=simple_data_set_request_fields, child_node='dataSet') + + feature_requested = get_requested( + selection_set=selection_set, requested_field_mapping=simple_feature_request_fields, child_node='feature') + + paging = paging if paging else Paging.DEFAULT + + query, count_query = build_heritability_result_request( + requested, data_set_requested, feature_requested, data_set=dataSet, distinct=distinct, feature=feature, max_p_value=maxPValue, min_p_value=minPValue, cluster=cluster, paging=paging) + + pagination_requested = get_requested(info, paging_fields, 'paging') + return paginate(query, count_query, paging, distinct, build_hr_graphql_response, pagination_requested) diff --git a/apps/iatlas/api-gitlab/api/resolvers/mutation_types_resolver.py b/apps/iatlas/api-gitlab/api/resolvers/mutation_types_resolver.py new file mode 100644 index 0000000000..c59744c85a --- /dev/null +++ b/apps/iatlas/api-gitlab/api/resolvers/mutation_types_resolver.py @@ -0,0 +1,11 @@ +from sqlalchemy import orm +from api import db +from api.database import return_mutation_type_query +from api.db_models import MutationType +from .resolver_helpers import build_mutation_type_graphql_response, get_requested, get_selection_set, mutation_type_request_fields, request_mutation_types + + +def resolve_mutation_types(_obj, info): + requested = get_requested(info, mutation_type_request_fields) + + return map(build_mutation_type_graphql_response, request_mutation_types(requested)) diff --git a/apps/iatlas/api-gitlab/api/resolvers/mutations_resolver.py b/apps/iatlas/api-gitlab/api/resolvers/mutations_resolver.py new file mode 100644 index 0000000000..88a5a6e214 --- /dev/null +++ b/apps/iatlas/api-gitlab/api/resolvers/mutations_resolver.py @@ -0,0 +1,39 @@ +from .resolver_helpers import build_mutation_graphql_response, get_requested, get_selection_set, mutation_related_sample_request_fields, mutation_request_fields, mutation_type_request_fields, build_mutation_request, simple_gene_request_fields +from .resolver_helpers.paging_utils import create_paging, paginate, paging_fields, create_paging + + +def resolve_mutations(_obj, info, cohort=None, distinct=False, entrez=None, mutation=None, mutationCode=None, mutationType=None, paging=None, sample=None, status=None): + + selection_set = get_selection_set(info=info, child_node='items') + + requested = get_requested( + selection_set=selection_set, requested_field_mapping=mutation_request_fields) + + gene_requested = get_requested( + selection_set=selection_set, requested_field_mapping=simple_gene_request_fields, child_node='gene') + + mutation_type_requested = get_requested( + selection_set=selection_set, requested_field_mapping=mutation_type_request_fields, child_node='mutationType') + + sample_selection_set = get_selection_set( + selection_set=selection_set, child_node='samples') + + sample_requested = get_requested( + selection_set=sample_selection_set, requested_field_mapping=mutation_related_sample_request_fields) + + max_items = 10 if 'samples' in requested else 100_000 + + paging = create_paging(paging, max_items) + + query, count_query = build_mutation_request( + requested, gene_requested, mutation_type_requested, cohort=cohort, distinct=distinct, entrez=entrez, mutation_code=mutationCode, mutation_type=mutationType, mutation=mutation, paging=paging, sample=sample) + + pagination_requested = get_requested(info, paging_fields, 'paging') + + response_function = build_mutation_graphql_response( + requested=requested, sample_requested=sample_requested, status=status, sample=sample, cohort=cohort) + + res = paginate(query, count_query, paging, distinct, + response_function, pagination_requested) + return(res) + diff --git a/apps/iatlas/api-gitlab/api/resolvers/neoantigens_resolver.py b/apps/iatlas/api-gitlab/api/resolvers/neoantigens_resolver.py new file mode 100644 index 0000000000..1432c9c1cf --- /dev/null +++ b/apps/iatlas/api-gitlab/api/resolvers/neoantigens_resolver.py @@ -0,0 +1,63 @@ + + +from .resolver_helpers import ( + build_neoantigen_graphql_response, + build_neoantigen_request, + neoantigen_request_fields, + get_requested, + get_selection_set, + simple_gene_request_fields, + simple_patient_request_fields +) + +from .resolver_helpers.paging_utils import paginate, create_paging, paging_fields + + +def resolve_neoantigens( + _obj, info, distinct=False, paging=None, patient=None, entrez=None, pmhc=None +): + # The selection is nested under the 'items' node. + selection_set = get_selection_set(info=info, child_node='items') + + requested = get_requested( + selection_set=selection_set, + requested_field_mapping=neoantigen_request_fields + ) + + patient_requested = get_requested( + selection_set=selection_set, + requested_field_mapping=simple_patient_request_fields, + child_node='patient' + ) + + gene_requested = get_requested( + selection_set=selection_set, + requested_field_mapping=simple_gene_request_fields, + child_node='gene' + ) + + max_items = 10000 + + paging = create_paging(paging, max_items) + + query, count_query = build_neoantigen_request( + requested, + patient_requested, + gene_requested, + distinct=distinct, + paging=paging, + patient=patient, + entrez=entrez, + pmhc=pmhc + ) + + pagination_requested = get_requested(info, paging_fields, 'paging') + + return paginate( + query=query, + count_query=count_query, + paging=paging, + distinct=distinct, + response_builder=build_neoantigen_graphql_response, + pagination_requested=pagination_requested + ) diff --git a/apps/iatlas/api-gitlab/api/resolvers/nodes_resolver.py b/apps/iatlas/api-gitlab/api/resolvers/nodes_resolver.py new file mode 100644 index 0000000000..5d96dab9d5 --- /dev/null +++ b/apps/iatlas/api-gitlab/api/resolvers/nodes_resolver.py @@ -0,0 +1,72 @@ +from .resolver_helpers import build_node_graphql_response, build_node_request, feature_request_fields, get_selection_set, gene_request_fields, get_requested, node_request_fields, simple_data_set_request_fields, simple_tag_request_fields +from .resolver_helpers.paging_utils import paging_fields, create_paging, paginate, paging_fields + + +def resolve_nodes( + _obj, + info, + dataSet=None, + distinct=False, + entrez=None, + feature=None, + featureClass=None, + geneType=None, + maxScore=None, + minScore=None, + network=None, + related=None, + paging=None, + tag1=None, + tag2=None, + nTags=None + ): + + selection_set = get_selection_set(info=info, child_node='items') + + requested = get_requested( + selection_set=selection_set, requested_field_mapping=node_request_fields) + + data_set_requested = get_requested( + selection_set=selection_set, requested_field_mapping=simple_data_set_request_fields, child_node='dataSet') + + feature_requested = get_requested( + selection_set=selection_set, requested_field_mapping=feature_request_fields, child_node='feature') + + gene_requested = get_requested( + selection_set=selection_set, requested_field_mapping=gene_request_fields, child_node='gene') + + tag_requested1 = get_requested( + selection_set=selection_set, requested_field_mapping=simple_tag_request_fields, child_node='tag1') + + tag_requested2 = get_requested( + selection_set=selection_set, requested_field_mapping=simple_tag_request_fields, child_node='tag2') + + max_items = 1000 + + paging = create_paging(paging, max_items) + + query, count_query = build_node_request( + requested, + data_set_requested, + feature_requested, + gene_requested, + tag_requested1, + tag_requested2, + data_set=dataSet, + distinct=distinct, + entrez=entrez, + feature=feature, + feature_class=featureClass, + gene_type=geneType, + max_score=maxScore, + min_score=minScore, + network=network, + related=related, + paging=paging, + tag1=tag1, + tag2=tag2, + n_tags=nTags + ) + + pagination_requested = get_requested(info, paging_fields, 'paging') + return paginate(query, count_query, paging, distinct, build_node_graphql_response(requested), pagination_requested) diff --git a/apps/iatlas/api-gitlab/api/resolvers/patient_resolver.py b/apps/iatlas/api-gitlab/api/resolvers/patient_resolver.py new file mode 100644 index 0000000000..a8f8974c40 --- /dev/null +++ b/apps/iatlas/api-gitlab/api/resolvers/patient_resolver.py @@ -0,0 +1,26 @@ +from .resolver_helpers import build_patient_graphql_response, build_patient_request, get_requested, patient_request_fields, simple_slide_request_fields, get_selection_set +from .resolver_helpers.paging_utils import paginate, Paging, paging_fields + + +def resolve_patients(_obj, info, distinct=False, paging=None, maxAgeAtDiagnosis=None, minAgeAtDiagnosis=None, barcode=None, dataSet=None, ethnicity=None, gender=None, maxHeight=None, minHeight=None, race=None, sample=None, slide=None, maxWeight=None, minWeight=None): + + selection_set = get_selection_set(info=info, child_node='items') + + requested = get_requested( + selection_set=selection_set, requested_field_mapping=patient_request_fields) + + slide_requested = get_requested( + selection_set=selection_set, requested_field_mapping=simple_slide_request_fields, child_node='slides') + + paging = paging if paging else Paging.DEFAULT + + query, count_query = build_patient_request( + requested, paging=paging, distinct=distinct, max_age_at_diagnosis=maxAgeAtDiagnosis, min_age_at_diagnosis=minAgeAtDiagnosis, barcode=barcode, data_set=dataSet, ethnicity=ethnicity, gender=gender, max_height=maxHeight, min_height=minHeight, race=race, sample=sample, slide=slide, max_weight=maxWeight, min_weight=minWeight + ) + + pagination_requested = get_requested(info, paging_fields, 'paging') + + res = paginate(query, count_query, paging, distinct, + build_patient_graphql_response(requested=requested, slide_requested=slide_requested, sample=sample, slide=slide), pagination_requested) + + return(res) diff --git a/apps/iatlas/api-gitlab/api/resolvers/rare_variant_pathway_association_resolver.py b/apps/iatlas/api-gitlab/api/resolvers/rare_variant_pathway_association_resolver.py new file mode 100644 index 0000000000..f35a0e3828 --- /dev/null +++ b/apps/iatlas/api-gitlab/api/resolvers/rare_variant_pathway_association_resolver.py @@ -0,0 +1,31 @@ +from .resolver_helpers import build_rvpa_graphql_response, build_rare_variant_pathway_association_request, rare_variant_pathway_association_request_fields, get_requested, get_selection_set, simple_data_set_request_fields, simple_feature_request_fields + +from .resolver_helpers.paging_utils import paginate, Paging, paging_fields + +def resolve_rare_variant_pathway_associations( + _obj, info, distinct=False, paging=None, dataSet=None, feature=None, pathway=None, maxPValue=None, minPValue=None): + + # The selection is nested under the 'items' node. + selection_set = get_selection_set(info=info, child_node='items') + requested = get_requested( + selection_set=selection_set, requested_field_mapping=rare_variant_pathway_association_request_fields) + + data_set_requested = get_requested( + selection_set=selection_set, requested_field_mapping=simple_data_set_request_fields, child_node='dataSet') + + feature_requested = get_requested( + selection_set=selection_set, requested_field_mapping=simple_feature_request_fields, child_node='feature') + + if distinct == False: + # Add the id as a cursor if not selecting distinct + requested.add('id') + + paging = paging if paging else Paging.DEFAULT + + query, count_query = build_rare_variant_pathway_association_request( + requested, data_set_requested, feature_requested, distinct=distinct, paging=paging, data_set=dataSet, feature=feature, pathway=pathway, max_p_value=maxPValue, min_p_value=minPValue) + + pagination_requested = get_requested(info, paging_fields, 'paging') + res = paginate(query, count_query, paging, distinct, + build_rvpa_graphql_response, pagination_requested) + return(res) diff --git a/apps/iatlas/api-gitlab/api/resolvers/resolver_helpers/__init__.py b/apps/iatlas/api-gitlab/api/resolvers/resolver_helpers/__init__.py new file mode 100644 index 0000000000..d7a8c4049a --- /dev/null +++ b/apps/iatlas/api-gitlab/api/resolvers/resolver_helpers/__init__.py @@ -0,0 +1,51 @@ +from .cell import build_cell_graphql_response, build_cell_request, cell_request_fields, feature_related_cell_request_fields, gene_related_cell_request_fields +from .cell_stat import build_cell_stat_graphql_response, build_cell_stat_request, cell_stat_request_fields +from .cohort import build_cohort_graphql_response, build_cohort_request, cohort_request_fields +from .colocalization import colocalization_request_fields, build_coloc_graphql_response, build_colocalization_request +from .copy_number_result import build_cnr_graphql_response, build_copy_number_result_request, cnr_request_fields +from .data_set import build_data_set_graphql_response, data_set_request_fields, build_data_set_request, simple_data_set_request_fields +from .driver_result import build_dr_graphql_response, build_driver_result_request, driver_result_request_fields +from .edge import build_edge_graphql_response, build_edge_request, edge_request_fields +from .feature import ( + build_feature_graphql_response, + feature_class_request_fields, + feature_request_fields, + simple_feature_request_fields, + simple_feature_request_fields2, + cell_feature_request_fields, + cell_related_feature_request_fields, + build_features_query +) +from .gene import ( + build_gene_graphql_response, + gene_request_fields, + simple_gene_request_fields, + cell_gene_request_fields, + build_gene_request +) +from .gene_set import gene_set_request_fields, request_gene_sets, simple_gene_set_request_fields +from .general_resolvers import * +from .germline_gwas_result import germline_gwas_result_request_fields, build_ggr_graphql_response, build_germline_gwas_result_request +from .heritability_result import heritability_result_request_fields, build_hr_graphql_response, build_heritability_result_request +from .mutation import build_mutation_graphql_response, build_mutation_request, mutation_request_fields +from .mutation_type import build_mutation_type_graphql_response, mutation_type_request_fields, request_mutation_types +from .neoantigen import build_neoantigen_graphql_response, build_neoantigen_request, neoantigen_request_fields +from .node import build_node_graphql_response, build_node_request, node_request_fields, simple_node_request_fields +from .patient import build_patient_request, build_patient_graphql_response, patient_request_fields, simple_patient_request_fields +from .publication import build_publication_graphql_response, publication_request_fields, simple_publication_request_fields +from .rare_variant_pathway_association import build_rvpa_graphql_response, build_rare_variant_pathway_association_request, rare_variant_pathway_association_request_fields +from .sample import ( + build_sample_graphql_response, + feature_related_sample_request_fields, + cell_type_feature_related_sample_request_fields, + gene_related_sample_request_fields, + cell_type_gene_related_sample_request_fields, + mutation_related_sample_request_fields, + build_sample_request, + sample_request_fields, + simple_sample_request_fields, + cohort_sample_request_fields +) +from .slide import build_slide_graphql_response, build_slide_request, slide_request_fields, simple_slide_request_fields +from .snp import snp_request_fields, build_snp_graphql_response, build_snp_request +from .tag import build_tag_graphql_response, simple_tag_request_fields, tag_request_fields, build_tag_request, has_tag_fields diff --git a/apps/iatlas/api-gitlab/api/resolvers/resolver_helpers/cell.py b/apps/iatlas/api-gitlab/api/resolvers/resolver_helpers/cell.py new file mode 100644 index 0000000000..d807573657 --- /dev/null +++ b/apps/iatlas/api-gitlab/api/resolvers/resolver_helpers/cell.py @@ -0,0 +1,176 @@ +from sqlalchemy import and_ +from sqlalchemy.orm import aliased +from api import db +from api.db_models import Cell, CellToFeature, CellToGene, CellToSample, Sample, Feature, Gene, CohortToSample, Cohort +from .general_resolvers import build_join_condition, get_selected, get_value +from .feature import build_feature_graphql_response +from .gene import build_gene_graphql_response +from .paging_utils import get_pagination_queries + +cell_request_fields = { + 'id', + 'type', + 'name', + 'features' +} + +feature_related_cell_request_fields = { + 'name', + 'type', + 'value' +} + +gene_related_cell_request_fields = { + 'name', + 'type', + 'singleCellSeq' +} + +def build_cell_graphql_response( + requested=[], + feature_requested=None, + prefix='cell_' +): + + from .feature import build_feature_graphql_response + + def f(cell): + if not cell: + return None + + id = get_value(cell, prefix + 'id') + + features = get_features( + [id], + requested=requested, + feature_requested=feature_requested + ) + + + result = { + 'id': id, + 'type': get_value(cell, prefix + 'type'), + 'name': get_value(cell, prefix + 'name'), + 'value': get_value(cell, prefix + 'feature_value'), + 'singleCellSeq': get_value(cell, prefix + 'single_cell_seq'), + 'features': map(build_feature_graphql_response(), features), + } + return result + return f + + +def build_cell_request( + requested, + distinct=False, + paging=None, + cohort=None, + cell=None, + feature=None +): + sess = db.session + + cell_1 = aliased(Cell, name='cell') + cell_to_feature_1 = aliased(CellToFeature, name='ctf') + cell_to_gene_1 = aliased(CellToGene, name='ctg') + cell_to_sample_1 = aliased(CellToSample, name='cts') + feature_1 = aliased(Feature, name='f') + gene_1 = aliased(Gene, name='g') + sample_1 = aliased(Sample, name = 's') + cohort_to_sample_1 = aliased(CohortToSample, name = 'cots') + cohort_1 = aliased(Cohort, name = 'c') + + + core_field_mapping = { + 'id': cell_1.id.label('cell_id'), + 'type': cell_1.cell_type.label('cell_type'), + 'name': cell_1.name.label('cell_name'), + } + + + core = get_selected(requested, core_field_mapping) + + query = sess.query(*core) + query = query.select_from(cell_1) + + if cell: + query = query.filter(cell_1.name.in_(cell)) + + if cohort: + + cohort_subquery = sess.query(cell_to_sample_1.cell_id) + + sample_join_condition = build_join_condition( + cell_to_sample_1.sample_id, + sample_1.id + ) + cohort_subquery = cohort_subquery.join( + sample_1, + and_(*sample_join_condition), + isouter=False + ) + + cohort_to_sample_join_condition = build_join_condition( + sample_1.id, + cohort_to_sample_1.sample_id, + ) + cohort_subquery = cohort_subquery.join( + cohort_to_sample_1, + and_(*cohort_to_sample_join_condition), + isouter=False + ) + + cohort_join_condition = build_join_condition( + cohort_to_sample_1.cohort_id, + cohort_1.id, + filter_column=cohort_1.name, + filter_list=cohort + ) + cohort_subquery = cohort_subquery.join( + cohort_1, + and_(*cohort_join_condition), + isouter=False + ) + + query = query.filter(cell_1.id.in_(cohort_subquery)) + + return get_pagination_queries(query, paging, distinct, cursor_field=cell_1.id) + +def get_features(cell_id, requested, feature_requested, cohort=None): + + if 'features' not in requested: + return [] + + sess = db.session + + feature_1 = aliased(Feature, name='f') + cell_to_feature_1 = aliased(CellToFeature, name='celltofeature') + + feature_core_field_mapping = { + 'name': feature_1.name.label('feature_name') + } + + feature_core = get_selected(feature_requested, feature_core_field_mapping) + + feature_core |= { + feature_1.id.label('feature_id') + } + + if 'value' in feature_requested: + feature_core |= { + cell_to_feature_1.feature_value.label('feature_value') + } + + query = sess.query(*feature_core) + query = query.select_from(feature_1) + + cell_to_feature_join_condition = build_join_condition( + feature_1.id, + cell_to_feature_1.feature_id, + cell_to_feature_1.cell_id, + cell_id + ) + query = query.join( + cell_to_feature_1, and_(*cell_to_feature_join_condition)) + + features = query.distinct().all() + return features diff --git a/apps/iatlas/api-gitlab/api/resolvers/resolver_helpers/cell_stat.py b/apps/iatlas/api-gitlab/api/resolvers/resolver_helpers/cell_stat.py new file mode 100644 index 0000000000..8f7bdb9891 --- /dev/null +++ b/apps/iatlas/api-gitlab/api/resolvers/resolver_helpers/cell_stat.py @@ -0,0 +1,98 @@ +from sqlalchemy import and_ +from sqlalchemy.orm import aliased +from api import db +from api.db_models import Dataset, CellStat, Gene +from .general_resolvers import build_join_condition, get_selected, get_value +from .data_set import build_data_set_graphql_response +from .gene import build_gene_graphql_response +from .paging_utils import get_pagination_queries + +cell_stat_request_fields = { + 'id', + 'dataSet', + 'gene', + 'snp', + 'type', + 'count', + 'avgExpr', + 'percExpr' +} + + +def build_cell_stat_graphql_response(cell_stat): + return { + 'id': get_value(cell_stat, 'id'), + 'dataSet': build_data_set_graphql_response()(cell_stat), + 'gene': build_gene_graphql_response()(cell_stat), + 'type': get_value(cell_stat, 'type'), + 'count': get_value(cell_stat, 'count'), + 'avgExpr': get_value(cell_stat, 'avg_expr'), + 'percExpr': get_value(cell_stat, 'perc_expr'), + } + + +def build_cell_stat_request( + requested, + data_set_requested, + gene_requested, + distinct=False, + paging=None, + entrez=None +): + """ + Builds a SQL request. + + All positional arguments are required. Positional arguments are: + 1st position - a set of the requested fields at the root of the graphql request + 2nd position - a set of the requested fields in the 'dataSet' node of the graphql request. If 'dataSet' is not requested, this will be an empty set. + 3rd position - a set of the requested fields in the 'gene' node of the graphql request. If 'gene' is not requested, this will be an empty set. + + All keyword arguments are optional. Keyword arguments are: + `distinct` - a boolean, indicates whether duplicate records should be filtered out + `paging` - a dict containing pagination metadata + `entrez` - a list of ints, entrez ids + """ + sess = db.session + + cell_stat_1 = aliased(CellStat, name='cell_stat') + data_set_1 = aliased(Dataset, name='ds') + gene_1 = aliased(Gene, name='g') + + core_field_mapping = { + 'id': cell_stat_1.id.label('id'), + 'type': cell_stat_1.cell_type.label('type'), + 'count': cell_stat_1.cell_count.label('count'), + 'avgExpr': cell_stat_1.avg_expr.label('avg_expr'), + 'percExpr': cell_stat_1.perc_expr.label('perc_expr'), + } + data_set_core_field_mapping = { + 'display': data_set_1.display.label('data_set_display'), + 'name': data_set_1.name.label('data_set_name'), + 'type': data_set_1.dataset_type.label('data_set_type') + } + gene_core_field_mapping = { + 'entrez': gene_1.entrez_id.label('gene_entrez'), + 'hgnc': gene_1.hgnc_id.label('gene_hgnc') + } + + core = get_selected(requested, core_field_mapping) + core |= get_selected(data_set_requested, data_set_core_field_mapping) + core |= get_selected(gene_requested, gene_core_field_mapping) + + query = sess.query(*core) + query = query.select_from(cell_stat_1) + + if 'dataSet' in requested: + data_set_join_condition = build_join_condition( + data_set_1.id, cell_stat_1.dataset_id) + query = query.join(data_set_1, and_( + *data_set_join_condition), isouter=False) + + if 'gene' in requested or entrez: + is_outer = not bool(entrez) + gene_join_condition = build_join_condition( + gene_1.id, cell_stat_1.gene_id, filter_column=gene_1.entrez_id, filter_list=entrez) + query = query.join(gene_1, and_( + *gene_join_condition), isouter=is_outer) + + return get_pagination_queries(query, paging, distinct, cursor_field=cell_stat_1.id) \ No newline at end of file diff --git a/apps/iatlas/api-gitlab/api/resolvers/resolver_helpers/cohort.py b/apps/iatlas/api-gitlab/api/resolvers/resolver_helpers/cohort.py new file mode 100644 index 0000000000..deb092fa84 --- /dev/null +++ b/apps/iatlas/api-gitlab/api/resolvers/resolver_helpers/cohort.py @@ -0,0 +1,252 @@ +from sqlalchemy import and_ +from sqlalchemy.orm import aliased +from api import db +from api.db_models import Cohort, Dataset, Tag, Sample, Feature, Gene, Mutation, CohortToSample, CohortToFeature, CohortToGene, CohortToMutation +from .general_resolvers import build_join_condition, get_selected, get_value +from .paging_utils import get_pagination_queries + +cohort_request_fields = {'id', 'name', + 'dataSet', 'tag', 'samples', 'features', 'genes', 'mutations'} + + +def build_cohort_graphql_response(requested=[], sample_requested=[], sample_tag_requested=[], feature_requested=[], gene_requested=[], mutation_requested=[], mutation_gene_requested=[]): + from .data_set import build_data_set_graphql_response + from .feature import build_feature_graphql_response + from .gene import build_gene_graphql_response + from .mutation import build_mutation_graphql_response + from .sample import build_sample_graphql_response + from .tag import build_tag_graphql_response + + def f(cohort): + if not cohort: + return None + else: + cohort_id = get_value(cohort, 'cohort_id') + samples = get_samples(cohort_id, requested, + sample_requested, sample_tag_requested) + features = get_features(cohort_id, requested, feature_requested) + genes = get_genes(cohort_id, requested, gene_requested) + mutations = get_mutations( + cohort_id, requested, mutation_requested, mutation_gene_requested) + dict = { + 'id': cohort_id, + 'name': get_value(cohort, 'cohort_name'), + 'dataSet': build_data_set_graphql_response()(cohort), + 'tag': build_tag_graphql_response()(cohort), + 'samples': map(build_sample_graphql_response(), samples), + 'features': map(build_feature_graphql_response(), features), + 'genes': map(build_gene_graphql_response(), genes), + 'mutations': map(build_mutation_graphql_response(), mutations) + } + return(dict) + + return f + + +def build_cohort_request(requested, data_set_requested, tag_requested, cohort=None, data_set=None, tag=None, distinct=False, paging=None): + """ + Builds a SQL request. + + All positional arguments are required. Positional arguments are: + 1st position - a set of the requested fields at the root of the graphql request + 2nd position - a set of the requested fields in the 'dataSet' node of the graphql request. If 'dataSet' is not requested, this will be an empty set. + 3rd position - a set of the requested fields in the 'tag' node of the graphql request. If 'tag' is not requested, this will be an empty set. + + All keyword arguments are optional. Keyword arguments are: + `cohort` - a list of strings, cohorts + `data_set` - a list of strings, data set names + `tag` - a list of strings, tag names + """ + from .tag import get_tag_column_labels + sess = db.session + + cohort_1 = aliased(Cohort, name='c') + data_set_1 = aliased(Dataset, name='ds') + tag_1 = aliased(Tag, name='t') + + core_field_mapping = { + 'name': cohort_1.name.label('cohort_name'), + } + + data_set_core_field_mapping = { + 'display': data_set_1.display.label('data_set_display'), + 'name': data_set_1.name.label('data_set_name'), + 'type': data_set_1.dataset_type.label('data_set_type') + } + + core = get_selected(requested, core_field_mapping) + core |= {cohort_1.id.label('cohort_id')} + core |= get_selected(data_set_requested, data_set_core_field_mapping) + core |= get_tag_column_labels(tag_requested, tag_1) + + query = sess.query(*core) + query = query.select_from(cohort_1) + + if cohort: + query = query.filter(cohort_1.name.in_(cohort)) + + if 'dataSet' in requested or data_set: + is_outer = not bool(data_set) + data_set_join_condition = build_join_condition( + data_set_1.id, cohort_1.dataset_id, filter_column=data_set_1.name, filter_list=data_set) + query = query.join(data_set_1, and_( + *data_set_join_condition), isouter=is_outer) + + if 'tag' in requested or tag: + is_outer = not bool(tag) + data_set_join_condition = build_join_condition( + tag_1.id, cohort_1.cohort_tag_id, filter_column=tag_1.name, filter_list=tag) + query = query.join(tag_1, and_( + *data_set_join_condition), isouter=is_outer) + + return get_pagination_queries(query, paging, distinct, cursor_field=cohort_1.id) + + +def get_samples(id, requested, sample_requested, tag_requested): + if 'samples' not in requested: + return([]) + else: + sess = db.session + + cohort_to_sample_1 = aliased(CohortToSample, name='cts') + sample_1 = aliased(Sample, name='s') + tag_1 = aliased(Tag, name='t2') + + sample_core_field_mapping = { + 'name': sample_1.name.label('sample_name'), + } + + tag_core_field_mapping = { + 'characteristics': tag_1.description.label('tag_characteristics'), + 'color': tag_1.color.label('tag_color'), + 'longDisplay': tag_1.long_display.label('tag_long_display'), + 'name': tag_1.name.label('tag_name'), + 'shortDisplay': tag_1.short_display.label('tag_short_display') + } + + core = get_selected(sample_requested, sample_core_field_mapping) + core |= get_selected(tag_requested, tag_core_field_mapping) + + query = sess.query(*core) + query = query.select_from(cohort_to_sample_1) + query = query.filter(cohort_to_sample_1.cohort_id == id) + + sample_join_condition = build_join_condition( + cohort_to_sample_1.sample_id, sample_1.id) + + query = query.join(sample_1, and_( + *sample_join_condition), isouter=False) + + if 'tag' in sample_requested: + sample_tag_join_condition = build_join_condition( + tag_1.id, cohort_to_sample_1.cohorts_to_samples_tag_id) + query = query.join(tag_1, and_( + *sample_tag_join_condition), isouter=True) + + samples = query.all() + return(samples) + + +def get_features(id, requested, feature_requested): + if 'features' not in requested: + return([]) + else: + sess = db.session + + cohort_to_feature_1 = aliased(CohortToFeature, name='ctf') + feature_1 = aliased(Feature, name='f') + + feature_core_field_mapping = { + 'display': feature_1.display.label('feature_display'), + 'name': feature_1.name.label('feature_name'), + } + + core = get_selected(feature_requested, feature_core_field_mapping) + + query = sess.query(*core) + query = query.select_from(cohort_to_feature_1) + query = query.filter(cohort_to_feature_1.cohort_id == id) + + feature_join_condition = build_join_condition( + cohort_to_feature_1.feature_id, feature_1.id) + + query = query.join(feature_1, and_( + *feature_join_condition), isouter=False) + + features = query.all() + return(features) + + +def get_genes(id, requested, gene_requested): + if 'genes' not in requested: + return([]) + else: + sess = db.session + + cohort_to_gene_1 = aliased(CohortToGene, name='ctg') + gene_1 = aliased(Gene, name='g') + + gene_core_field_mapping = { + 'hgnc': gene_1.hgnc_id.label('gene_hgnc'), + 'entrez': gene_1.entrez_id.label('gene_entrez'), + } + + core = get_selected(gene_requested, gene_core_field_mapping) + + query = sess.query(*core) + query = query.select_from(cohort_to_gene_1) + query = query.filter(cohort_to_gene_1.cohort_id == id) + + gene_join_condition = build_join_condition( + cohort_to_gene_1.gene_id, gene_1.id) + + query = query.join(gene_1, and_( + *gene_join_condition), isouter=False) + + genes = query.all() + return(genes) + + +def get_mutations(id, requested, mutation_requested, mutation_gene_requested): + + if 'mutations' not in requested: + return([]) + else: + sess = db.session + + cohort_to_mutation_1 = aliased(CohortToMutation, name='ctm') + mutation_1 = aliased(Mutation, name='m') + gene_1 = aliased(Gene, name='g') + + mutation_core_field_mapping = { + 'mutationCode': mutation_1.mutation_code.label('mutation_code') + } + + mutation_gene_core_field_mapping = { + 'hgnc': gene_1.hgnc_id.label('gene_hgnc'), + 'entrez': gene_1.entrez_id.label('gene_entrez'), + } + + core = get_selected(mutation_requested, mutation_core_field_mapping) + core |= get_selected(mutation_gene_requested, + mutation_gene_core_field_mapping) + + query = sess.query(*core) + query = query.select_from(cohort_to_mutation_1) + query = query.filter(cohort_to_mutation_1.cohort_id == id) + + mutation_join_condition = build_join_condition( + mutation_1.id, cohort_to_mutation_1.mutation_id) + + query = query.join(mutation_1, and_( + *mutation_join_condition), isouter=False) + + if 'gene' in mutation_requested: + mutation_gene_join_condition = build_join_condition( + mutation_1.gene_id, gene_1.id) + + query = query.join(gene_1, and_( + *mutation_gene_join_condition), isouter=False) + + mutations = query.all() + return(mutations) diff --git a/apps/iatlas/api-gitlab/api/resolvers/resolver_helpers/colocalization.py b/apps/iatlas/api-gitlab/api/resolvers/resolver_helpers/colocalization.py new file mode 100644 index 0000000000..c619b9d2c0 --- /dev/null +++ b/apps/iatlas/api-gitlab/api/resolvers/resolver_helpers/colocalization.py @@ -0,0 +1,172 @@ +from sqlalchemy import and_ +from sqlalchemy.orm import aliased +from api import db +from api.db_models import Dataset, Colocalization, Feature, Gene, Snp +from .general_resolvers import build_join_condition, get_selected, get_value +from .data_set import build_data_set_graphql_response +from .feature import build_feature_graphql_response +from .gene import build_gene_graphql_response +from .snp import build_snp_graphql_response +from .paging_utils import get_pagination_queries + +colocalization_request_fields = { + 'id', + 'dataSet', + 'colocDataSet', + 'feature', + 'gene', + 'snp', + 'qtlType', + 'eCaviarPP', + 'plotType', + 'tissue', + 'spliceLoc', + 'plotLink' +} + + +def build_coloc_graphql_response(colocalization): + return { + 'id': get_value(colocalization, 'id'), + 'dataSet': build_data_set_graphql_response()(colocalization), + 'colocDataSet': build_data_set_graphql_response(prefix='coloc_data_set_')(colocalization), + 'feature': build_feature_graphql_response()(colocalization), + 'gene': build_gene_graphql_response()(colocalization), + 'snp': build_snp_graphql_response(colocalization), + 'qtlType': get_value(colocalization, 'qtl_type'), + 'eCaviarPP': get_value(colocalization, 'ecaviar_pp'), + 'plotType': get_value(colocalization, 'plot_type'), + 'tissue': get_value(colocalization, 'tissue'), + 'spliceLoc': get_value(colocalization, 'splice_loc'), + 'plotLink': get_value(colocalization, 'plot_link') + } + + +def build_colocalization_request( + requested, data_set_requested, coloc_data_set_requested, feature_requested, gene_requested, snp_requested, distinct=False, paging=None, data_set=None, coloc_data_set=None, feature=None, entrez=None, snp=None, qtl_type=None, ecaviar_pp=None, plot_type=None): + """ + Builds a SQL request. + + All positional arguments are required. Positional arguments are: + 1st position - a set of the requested fields at the root of the graphql request + 2nd position - a set of the requested fields in the 'dataSet' node of the graphql request. If 'dataSet' is not requested, this will be an empty set. + 3rd position - a set of the requested fields in the 'colocDataSet' node of the graphql request. If 'colocDataSet' is not requested, this will be an empty set. + 4th position - a set of the requested fields in the 'feature' node of the graphql request. If 'feature' is not requested, this will be an empty set. + 5th position - a set of the requested fields in the 'gene' node of the graphql request. If 'gene' is not requested, this will be an empty set. + 6th position - a set of the requested fields in the 'snp' node of the graphql request. If 'snp' is not requested, this will be an empty set. + + All keyword arguments are optional. Keyword arguments are: + `distinct` - a boolean, indicates whether duplicate records should be filtered out + `paging` - a dict containing pagination metadata + `data_set` - a list of strings, data set names + `coloc_data_set` - a list of strings, data set names + `feature` - a list of strings, feature names + `entrez` - a list of ints, entrez ids + `snp` - a list of strings, snp names + `qtl_type` - a string, (either 'sQTL' or 'eQTL') + `ecaviar_pp` - a string, (either 'C1' or 'C2') + `plot_type` - a string, (either '3 Level Plot' or 'Expanded Region') + """ + sess = db.session + + colocalization_1 = aliased(Colocalization, name='coloc') + data_set_1 = aliased(Dataset, name='ds') + coloc_data_set_1 = aliased(Dataset, name='cds') + feature_1 = aliased(Feature, name='f') + gene_1 = aliased(Gene, name='g') + snp_1 = aliased(Snp, name='s') + + core_field_mapping = { + 'id': colocalization_1.id.label('id'), + 'qtlType': colocalization_1.qtl_type.label('qtl_type'), + 'eCaviarPP': colocalization_1.ecaviar_pp.label('ecaviar_pp'), + 'plotType': colocalization_1.plot_type.label('plot_type'), + 'tissue': colocalization_1.tissue.label('tissue'), + 'spliceLoc': colocalization_1.splice_loc.label('splice_loc'), + 'plotLink': colocalization_1.link.label('plot_link') + } + data_set_core_field_mapping = { + 'display': data_set_1.display.label('data_set_display'), + 'name': data_set_1.name.label('data_set_name'), + 'type': data_set_1.dataset_type.label('data_set_type') + } + coloc_data_set_core_field_mapping = { + 'display': coloc_data_set_1.display.label('coloc_data_set_display'), + 'name': coloc_data_set_1.name.label('coloc_data_set_name'), + 'type': coloc_data_set_1.dataset_type.label('coloc_data_set_type') + } + feature_core_field_mapping = { + 'display': feature_1.display.label('feature_display'), + 'name': feature_1.name.label('feature_name'), + 'order': feature_1.order.label('feature_order'), + 'unit': feature_1.unit.label('feature_unit'), + 'germlineCategory': feature_1.germline_category.label('feature_germline_category'), + 'germlineModule': feature_1.germline_module.label('feature_germline_module') + } + gene_core_field_mapping = { + 'entrez': gene_1.entrez_id.label('gene_entrez'), + 'hgnc': gene_1.hgnc_id.label('gene_hgnc') + } + snp_core_field_mapping = { + 'rsid': snp_1.rsid.label('snp_rsid'), + 'name': snp_1.name.label('snp_name'), + 'bp': snp_1.bp.label('snp_bp'), + 'chr': snp_1.chr.label('snp_chr') + } + + core = get_selected(requested, core_field_mapping) + core |= get_selected(data_set_requested, data_set_core_field_mapping) + core |= get_selected(coloc_data_set_requested, + coloc_data_set_core_field_mapping) + core |= get_selected(feature_requested, feature_core_field_mapping) + core |= get_selected(gene_requested, gene_core_field_mapping) + core |= get_selected(snp_requested, snp_core_field_mapping) + + query = sess.query(*core) + query = query.select_from(colocalization_1) + + if qtl_type: + query = query.filter(colocalization_1.qtl_type == qtl_type) + + if ecaviar_pp: + query = query.filter(colocalization_1.ecaviar_pp == ecaviar_pp) + + if plot_type: + query = query.filter(colocalization_1.plot_type == plot_type) + + if 'dataSet' in requested or data_set: + is_outer = not bool(data_set) + data_set_join_condition = build_join_condition( + data_set_1.id, colocalization_1.dataset_id, filter_column=data_set_1.name, filter_list=data_set) + query = query.join(data_set_1, and_( + *data_set_join_condition), isouter=is_outer) + + if 'colocDataSet' in requested or coloc_data_set: + is_outer = not bool(data_set) + data_set_join_condition = build_join_condition( + coloc_data_set_1.id, colocalization_1.coloc_dataset_id, filter_column=coloc_data_set_1.name, filter_list=coloc_data_set) + query = query.join(coloc_data_set_1, and_( + *data_set_join_condition), isouter=is_outer) + + if 'feature' in requested or feature: + is_outer = not bool(feature) + feature_join_condition = build_join_condition( + feature_1.id, colocalization_1.feature_id, filter_column=feature_1.name, filter_list=feature) + query = query.join(feature_1, and_( + *feature_join_condition), isouter=is_outer) + + if 'gene' in requested or entrez: + is_outer = not bool(entrez) + gene_join_condition = build_join_condition( + gene_1.id, colocalization_1.gene_id, filter_column=gene_1.entrez_id, filter_list=entrez) + query = query.join(gene_1, and_( + *gene_join_condition), isouter=is_outer) + + if 'snp' in requested or snp: + is_outer = not bool(snp) + snp_join_condition = build_join_condition( + snp_1.id, colocalization_1.snp_id, filter_column=snp_1.name, filter_list=snp) + query = query.join(snp_1, and_( + *snp_join_condition), isouter=is_outer) + + return get_pagination_queries(query, paging, distinct, cursor_field=colocalization_1.id) diff --git a/apps/iatlas/api-gitlab/api/resolvers/resolver_helpers/copy_number_result.py b/apps/iatlas/api-gitlab/api/resolvers/resolver_helpers/copy_number_result.py new file mode 100644 index 0000000000..d9e2184222 --- /dev/null +++ b/apps/iatlas/api-gitlab/api/resolvers/resolver_helpers/copy_number_result.py @@ -0,0 +1,194 @@ +from sqlalchemy import and_ +from sqlalchemy.orm import aliased +from api import db +from api.db_models import CopyNumberResult, Dataset, DatasetToTag, Feature, Gene, Tag +from .general_resolvers import build_join_condition, get_selected, get_value +from .paging_utils import get_pagination_queries + +cnr_request_fields = {'dataSet', + 'direction', + 'feature', + 'gene', + 'meanNormal', + 'meanCnv', + 'pValue', + 'log10PValue', + 'tag', + 'tStat'} + + +def build_cnr_graphql_response(prefix=''): + from .data_set import build_data_set_graphql_response + from .feature import build_feature_graphql_response + from .gene import build_gene_graphql_response + from .tag import build_tag_graphql_response + + def f(copy_number_result): + if not copy_number_result: + return None + else: + dict = { + 'id': get_value(copy_number_result, prefix + 'id'), + 'direction': get_value(copy_number_result, prefix + 'direction'), + 'meanNormal': get_value(copy_number_result, prefix + 'mean_normal'), + 'meanCnv': get_value(copy_number_result, prefix + 'mean_cnv'), + 'pValue': get_value(copy_number_result, prefix + 'p_value'), + 'log10PValue': get_value(copy_number_result, prefix + 'log10_p_value'), + 'tStat': get_value(copy_number_result, prefix + 't_stat'), + 'dataSet': build_data_set_graphql_response()(copy_number_result), + 'feature': build_feature_graphql_response()(copy_number_result), + 'gene': build_gene_graphql_response()(copy_number_result), + 'tag': build_tag_graphql_response()(copy_number_result) + } + return(dict) + return(f) + + +def build_copy_number_result_request( + requested, data_set_requested, feature_requested, gene_requested, tag_requested, data_set=None, direction=None, distinct=False, entrez=None, feature=None, max_p_value=None, max_log10_p_value=None, min_log10_p_value=None, min_mean_cnv=None, min_mean_normal=None, min_p_value=None, min_t_stat=None, paging=None, related=None, tag=None): + """ + Builds a SQL request. + + All positional arguments are required. Positional arguments are: + 1st position - a set of the requested fields at the root of the graphql request + 2nd position - a set of the requested fields in the 'dataSet' node of the graphql request. If 'dataSet' is not requested, this will be an empty set. + 3rd position - a set of the requested fields in the 'feature' node of the graphql request. If 'feature' is not requested, this will be an empty set. + 4th position - a set of the requested fields in the 'gene' node of the graphql request. If 'gene' is not requested, this will be an empty set. + 5th position - a set of the requested fields in the 'tag' node of the graphql request. If 'tag' is not requested, this will be an empty set. + + All keyword arguments are optional. Keyword arguments are: + `data_set` - a list of strings, data set names + `direction` - a value from the DirectionEnum. (either 'Amp' or 'Del') + `distinct` - a boolean, indicates whether duplicate records should be filtered out + `entrez` - a list of integers, gene entrez ids + `feature` - a list of strings, feature names + `max_p_value` - a float, a maximum P value + `max_log10_p_value` - a float, a minimum calculated log10 P value + `min_log10_p_value` - a float, a minimum calculated log 10 P value + `min_mean_cnv` - a float, a minimum mean cnv value + `min_mean_normal` - a float, a minimum mean normal value + `min_p_value` - a float, a minimum P value + `min_t_stat` - a float, a minimum t stat value + `paging` - a dict containing pagination metadata + `related` - a list of strings, tags related to the dataset that is associated with the result. + `tag` - a list of strings, tag names + """ + from .tag import get_tag_column_labels + sess = db.session + + copy_number_result_1 = aliased(CopyNumberResult, name='dcnr') + data_set_1 = aliased(Dataset, name='ds') + feature_1 = aliased(Feature, name='f') + gene_1 = aliased(Gene, name='g') + tag_1 = aliased(Tag, name='t') + + core_field_mapping = { + 'direction': copy_number_result_1.direction.label('direction'), + 'meanNormal': copy_number_result_1.mean_normal.label('mean_normal'), + 'meanCnv': copy_number_result_1.mean_cnv.label('mean_cnv'), + 'pValue': copy_number_result_1.p_value.label('p_value'), + 'log10PValue': copy_number_result_1.log10_p_value.label('log10_p_value'), + 'tStat': copy_number_result_1.t_stat.label('t_stat')} + + data_set_field_mapping = { + 'display': data_set_1.display.label('data_set_display'), + 'name': data_set_1.name.label('data_set_name'), + 'type': data_set_1.dataset_type.label('data_set_type') + } + + feature_field_mapping = { + 'display': feature_1.display.label('feature_display'), + 'name': feature_1.name.label('feature_name'), + 'order': feature_1.order.label('order'), + 'unit': feature_1.unit.label('unit') + } + + gene_field_mapping = { + 'entrez': gene_1.entrez_id.label('gene_entrez'), + 'hgnc': gene_1.hgnc_id.label('gene_hgnc'), + 'description': gene_1.description.label('gene_description'), + 'friendlyName': gene_1.friendly_name.label('gene_friendly_name'), + 'ioLandscapeName': gene_1.io_landscape_name.label('gene_io_landscape_name') + } + + core = get_selected(requested, core_field_mapping) + core |= get_selected(data_set_requested, data_set_field_mapping) + core |= get_selected(feature_requested, feature_field_mapping) + core |= get_selected(gene_requested, gene_field_mapping) + core |= get_tag_column_labels(tag_requested, tag_1) + + if not distinct: + # Add the id as a cursor if not selecting distinct + core.add(copy_number_result_1.id.label('id')) + + query = sess.query(*core) + query = query.select_from(copy_number_result_1) + + if direction: + query = query.filter(copy_number_result_1.direction == direction) + + if max_p_value or max_p_value == 0: + query = query.filter(copy_number_result_1.p_value <= max_p_value) + + if (max_log10_p_value or max_log10_p_value == 0) and (not max_p_value and max_p_value != 0): + query = query.filter( + copy_number_result_1.log10_p_value <= max_log10_p_value) + + if (min_log10_p_value or min_log10_p_value == 0) and (not min_p_value and min_p_value != 0): + query = query.filter( + copy_number_result_1.log10_p_value >= min_log10_p_value) + + if min_mean_cnv or min_mean_cnv == 0: + query = query.filter(copy_number_result_1.mean_cnv >= min_mean_cnv) + + if min_mean_normal or min_mean_normal == 0: + query = query.filter( + copy_number_result_1.mean_normal >= min_mean_normal) + + if min_p_value or min_p_value == 0: + query = query.filter(copy_number_result_1.p_value >= min_p_value) + + if min_t_stat or min_t_stat == 0: + query = query.filter(copy_number_result_1.t_stat >= min_t_stat) + + if data_set or 'dataSet' in requested or related: + is_outer = not bool(data_set) + data_set_join_condition = build_join_condition( + data_set_1.id, copy_number_result_1.dataset_id, filter_column=data_set_1.name, filter_list=data_set) + query = query.join(data_set_1, and_( + *data_set_join_condition), isouter=is_outer) + + if entrez or 'gene' in requested: + is_outer = not bool(entrez) + data_set_join_condition = build_join_condition( + gene_1.id, copy_number_result_1.gene_id, filter_column=gene_1.entrez_id, filter_list=entrez) + query = query.join(gene_1, and_( + *data_set_join_condition), isouter=is_outer) + + if feature or 'feature' in requested: + is_outer = not bool(feature) + data_set_join_condition = build_join_condition( + feature_1.id, copy_number_result_1.feature_id, filter_column=feature_1.name, filter_list=feature) + query = query.join(feature_1, and_( + *data_set_join_condition), isouter=is_outer) + + if tag or 'tag' in requested: + is_outer = not bool(tag) + data_set_join_condition = build_join_condition( + tag_1.id, copy_number_result_1.tag_id, filter_column=tag_1.name, filter_list=tag) + query = query.join(tag_1, and_( + *data_set_join_condition), isouter=is_outer) + + if related: + data_set_to_tag_1 = aliased(DatasetToTag, name='dtt') + related_tag_1 = aliased(Tag, name='rt') + + related_tag_sub_query = sess.query(related_tag_1.id).filter( + related_tag_1.name.in_(related)) + + data_set_tag_join_condition = build_join_condition( + data_set_to_tag_1.dataset_id, data_set_1.id, data_set_to_tag_1.tag_id, related_tag_sub_query) + query = query.join( + data_set_to_tag_1, and_(*data_set_tag_join_condition)) + + return get_pagination_queries(query, paging, distinct, cursor_field=copy_number_result_1.id) diff --git a/apps/iatlas/api-gitlab/api/resolvers/resolver_helpers/data_set.py b/apps/iatlas/api-gitlab/api/resolvers/resolver_helpers/data_set.py new file mode 100644 index 0000000000..3260672096 --- /dev/null +++ b/apps/iatlas/api-gitlab/api/resolvers/resolver_helpers/data_set.py @@ -0,0 +1,145 @@ +from sqlalchemy.orm import aliased +from api import db +from sqlalchemy import and_ +from api.db_models import Dataset, Sample, DatasetToSample, DatasetToTag, Tag +from .general_resolvers import get_selected, get_value, build_join_condition +from .paging_utils import get_pagination_queries + + +simple_data_set_request_fields = {'display', 'name', 'type'} + +data_set_request_fields = simple_data_set_request_fields.union({ + 'samples', 'tags'}) + + +def build_data_set_graphql_response(prefix='data_set_', requested=[], sample_requested=[], tag_requested=[], sample=None): + from .sample import build_sample_graphql_response + from .tag import build_tag_graphql_response + + def f(data_set): + if not data_set: + return None + else: + id = get_value(data_set, prefix + 'id') + samples = get_samples(id, requested, sample_requested, sample) + tags = get_tags(id, requested, tag_requested) + dict = { + 'id': id, + 'display': get_value(data_set, prefix + 'display'), + 'name': get_value(data_set, prefix + 'name'), + 'type': get_value(data_set, prefix + 'type'), + 'samples': map(build_sample_graphql_response(), samples), + 'tags': map(build_tag_graphql_response(), tags), + } + return(dict) + return(f) + + +def build_data_set_request(requested, data_set=None, sample=None, data_set_type=None, distinct=False, paging=None): + ''' + Builds a SQL query. + + All positional arguments are required. Positional arguments are: + 1st position - a set of the requested fields at the root of the graphql request. + + All keyword arguments are optional. Keyword arguments are: + `data_set` - a list of strings, data set names + `sample` - a list of strings, sample names + `data_set_type` - a list of strings, data set types + `distinct` - a boolean, indicates whether duplicate records should be filtered out + `paging` - a dict containing pagination metadata + ''' + sess = db.session + + data_set_1 = aliased(Dataset, name='d') + data_set_to_sample_1 = aliased(DatasetToSample, name='dts') + sample_1 = aliased(Sample, name='s') + + core_field_mapping = { + 'display': data_set_1.display.label('data_set_display'), + 'name': data_set_1.name.label('data_set_name'), + 'type': data_set_1.dataset_type.label('data_set_type') + } + + core = get_selected(requested, core_field_mapping) + core |= {data_set_1.id.label('data_set_id')} + + query = sess.query(*core) + query = query.select_from(data_set_1) + + if sample: + + data_set_to_sample_subquery = sess.query( + data_set_to_sample_1.dataset_id) + + sample_join_condition = build_join_condition( + data_set_to_sample_1.sample_id, sample_1.id, filter_column=sample_1.name, filter_list=sample) + + data_set_to_sample_subquery = data_set_to_sample_subquery.join(sample_1, and_( + *sample_join_condition), isouter=False) + + query = query.filter(data_set_1.id.in_(data_set_to_sample_subquery)) + + if data_set: + query = query.filter(data_set_1.name.in_(data_set)) + + if data_set_type: + query = query.filter(data_set_1.dataset_type.in_(data_set_type)) + + return get_pagination_queries(query, paging, distinct, cursor_field=data_set_1.id) + + +def get_samples(dataset_id, requested, sample_requested, sample=None): + if 'samples' in requested: + sess = db.session + + data_set_to_sample_1 = aliased(DatasetToSample, name='dts') + sample_1 = aliased(Sample, name='s') + + sample_core_field_mapping = { + 'name': sample_1.name.label('sample_name')} + + sample_core = get_selected(sample_requested, sample_core_field_mapping) + #sample_core |= {sample_1.id.label('sample_id')} + + sample_query = sess.query(*sample_core) + sample_query = sample_query.select_from(sample_1) + + if sample: + sample_query = sample_query.filter(sample_1.name.in_(sample)) + + data_set_to_sample_subquery = sess.query( + data_set_to_sample_1.sample_id) + + data_set_to_sample_subquery = data_set_to_sample_subquery.filter( + data_set_to_sample_1.dataset_id == dataset_id) + + sample_query = sample_query.filter( + sample_1.id.in_(data_set_to_sample_subquery)) + + return sample_query.distinct().all() + + return [] + + +def get_tags(dataset_id, requested, tag_requested): + if 'tags' in requested: + from .tag import get_tag_column_labels + sess = db.session + + data_set_to_tag_1 = aliased(DatasetToTag, name='dtt') + tag_1 = aliased(Tag, name='t') + + tag_core = get_tag_column_labels(tag_requested, tag_1) + query = sess.query(*tag_core) + query = query.select_from(tag_1) + + subquery = sess.query(data_set_to_tag_1.tag_id) + + subquery = subquery.filter(data_set_to_tag_1.dataset_id == dataset_id) + + query = query.filter(tag_1.id.in_(subquery)) + + return query.distinct().all() + + return [] diff --git a/apps/iatlas/api-gitlab/api/resolvers/resolver_helpers/driver_result.py b/apps/iatlas/api-gitlab/api/resolvers/resolver_helpers/driver_result.py new file mode 100644 index 0000000000..b35eb9aa2a --- /dev/null +++ b/apps/iatlas/api-gitlab/api/resolvers/resolver_helpers/driver_result.py @@ -0,0 +1,198 @@ +from sqlalchemy import and_ +from sqlalchemy.orm import aliased +from api import db +from api.db_models import Dataset, DatasetToTag, DriverResult, Feature, Gene, Mutation, MutationType, Tag +from .general_resolvers import build_join_condition, get_selected, get_value +from .paging_utils import get_pagination_queries + +driver_result_request_fields = { + 'dataSet', + 'feature', + 'mutation', + 'tag', + 'pValue', + 'foldChange', + 'log10PValue', + 'log10FoldChange', + 'numWildTypes', + 'numMutants' +} + + +def build_dr_graphql_response(driver_result): + from .data_set import build_data_set_graphql_response + from .feature import build_feature_graphql_response + from .mutation import build_mutation_graphql_response + from .tag import build_tag_graphql_response + + dict = { + 'id': get_value(driver_result, 'id'), + 'pValue': get_value(driver_result, 'p_value'), + 'foldChange': get_value(driver_result, 'fold_change'), + 'log10PValue': get_value(driver_result, 'log10_p_value'), + 'log10FoldChange': get_value(driver_result, 'log10_fold_change'), + 'numWildTypes': get_value(driver_result, 'n_wt'), + 'numMutants': get_value(driver_result, 'n_mut'), + 'dataSet': build_data_set_graphql_response()(driver_result), + 'feature': build_feature_graphql_response()(driver_result), + 'mutation': build_mutation_graphql_response()(driver_result), + 'tag': build_tag_graphql_response()(driver_result) + } + return(dict) + + +def build_driver_result_request(requested, data_set_requested, feature_requested, mutation_requested, mutation_gene_requested, mutation_type_requested, tag_requested, data_set=None, distinct=False, entrez=None, feature=None, max_p_value=None, max_log10_p_value=None, min_fold_change=None, min_log10_fold_change=None, min_log10_p_value=None, min_p_value=None, min_n_mut=None, min_n_wt=None, mutation=None, mutation_code=None, paging=None, related=None, tag=None): + """ + Builds a SQL request. + + All positional arguments are required. Positional arguments are: + 1st position - a set of the requested fields at the root of the graphql request + 2nd position - a set of the requested fields in the 'dataSet' node of the graphql request. If 'dataSet' is not requested, this will be an empty set. + 3rd position - a set of the requested fields in the 'feature' node of the graphql request. If 'feature' is not requested, this will be an empty set. + 4th position - a set of the requested fields in the 'mutation' node of the graphql request. If 'mutation' is not requested, this will be an empty set. + 5th position - a set of the requested fields in the 'gene' node of the graphql request. If 'gene' is not requested, this will be an empty set. + 6th position - a set of the requested fields in the 'mutationType' node of the graphql request. If 'mutationType' is not requested, this will be an empty set. + 7th position - a set of the requested fields in the 'tag' node of the graphql request. If 'tag' is not requested, this will be an empty set. + + All keyword arguments are optional. Keyword arguments are: + `data_set` - a list of strings, data set names + `distinct` - a boolean, indicates whether duplicate records should be filtered out + `entrez` - a list of integers, gene entrez ids + `feature` - a list of strings, feature names + `max_p_value` - a float, a maximum P value + `max_log10_p_value` - a float, a minimum calculated log10 P value + `min_fold_change` - a float, a minimum fold change value + `min_log10_fold_change` - a float, a minimum calculated log 10 fold change value + `min_log10_p_value` - a float, a minimum calculated log 10 P value + `min_p_value` - a float, a minimum P value + `min_n_mut` - a float, a minimum number of mutants + `min_n_wt` - a float, a minimum number of wild types + `mutation` - a list of strings, mutations + `mutation_code` - a list of strings, mutation codes + `paging` - a dict containing pagination metadata + `related` - a list of strings, tags related to the dataset that is associated with the result. + `tag` - a list of strings, tag names + """ + from .tag import get_tag_column_labels + from .gene import get_simple_gene_column_labels + from .mutation import get_mutation_column_labels, get_mutation_type_column_labels, build_simple_mutation_request + sess = db.session + + driver_result_1 = aliased(DriverResult, name='dr') + gene_1 = aliased(Gene, name='g') + mutation_1 = aliased(Mutation, name='m') + mutation_type_1 = aliased(MutationType, name='mt') + tag_1 = aliased(Tag, name='t') + feature_1 = aliased(Feature, name='f') + data_set_1 = aliased(Dataset, name='ds') + + core_field_mapping = { + 'id': driver_result_1.id.label('id'), + 'pValue': driver_result_1.p_value.label('p_value'), + 'foldChange': driver_result_1.fold_change.label('fold_change'), + 'log10PValue': driver_result_1.log10_p_value.label('log10_p_value'), + 'log10FoldChange': driver_result_1.log10_fold_change.label('log10_fold_change'), + 'numWildTypes': driver_result_1.n_wildtype.label('n_wt'), + 'numMutants': driver_result_1.n_mutants.label('n_mut') + } + data_set_core_field_mapping = { + 'display': data_set_1.display.label('data_set_display'), + 'name': data_set_1.name.label('data_set_name'), + 'type': data_set_1.dataset_type.label('data_set_type') + } + feature_core_field_mapping = { + 'display': feature_1.display.label('feature_display'), + 'name': feature_1.name.label('feature_name'), + 'order': feature_1.order.label('feature_order'), + 'unit': feature_1.unit.label('feature_unit') + } + + core = get_selected(requested, core_field_mapping) + core |= get_selected(data_set_requested, data_set_core_field_mapping) + core |= get_selected(feature_requested, feature_core_field_mapping) + core |= get_tag_column_labels(tag_requested, tag_1) + core |= get_mutation_column_labels( + mutation_requested, mutation_1 + ) + core |= get_simple_gene_column_labels(mutation_gene_requested, gene_1) + core |= get_mutation_type_column_labels( + mutation_type_requested, mutation_type_1) + + core |= {driver_result_1.id.label('id')} + + query = sess.query(*core) + query = query.select_from(driver_result_1) + + if max_p_value or max_p_value == 0: + query = query.filter(driver_result_1.p_value <= max_p_value) + + if (max_log10_p_value or max_log10_p_value == 0) and (not max_p_value and max_p_value != 0): + query = query.filter( + driver_result_1.log10_p_value <= max_log10_p_value) + + if min_fold_change or min_fold_change == 0: + query = query.filter( + driver_result_1.fold_change >= min_fold_change) + + if (min_log10_fold_change or min_log10_fold_change == 0) and (not min_fold_change and min_fold_change != 0): + query = query.filter( + driver_result_1.log10_fold_change >= min_log10_fold_change) + + if (min_log10_p_value or min_log10_p_value == 0) and (not min_p_value and min_p_value != 0): + query = query.filter( + driver_result_1.log10_p_value >= min_log10_p_value) + + if min_p_value or min_p_value == 0: + query = query.filter(driver_result_1.p_value >= min_p_value) + + if min_n_mut or min_n_mut == 0: + query = query.filter(driver_result_1.n_mutants >= min_n_mut) + + if min_n_wt or min_n_wt == 0: + query = query.filter(driver_result_1.n_wildtype >= min_n_wt) + + if 'dataSet' in requested or data_set or related: + is_outer = not bool(data_set) + data_set_join_condition = build_join_condition( + data_set_1.id, driver_result_1.dataset_id, filter_column=data_set_1.name, filter_list=data_set) + query = query.join(data_set_1, and_( + *data_set_join_condition), isouter=is_outer) + + if 'feature' in requested or feature: + is_outer = not bool(feature) + data_set_join_condition = build_join_condition( + feature_1.id, driver_result_1.feature_id, filter_column=feature_1.name, filter_list=feature) + query = query.join(feature_1, and_( + *data_set_join_condition), isouter=is_outer) + + if 'tag' in requested or tag: + is_outer = not bool(tag) + data_set_join_condition = build_join_condition( + tag_1.id, driver_result_1.tag_id, filter_column=tag_1.name, filter_list=tag) + query = query.join(tag_1, and_( + *data_set_join_condition), isouter=is_outer) + + if related: + data_set_to_tag_1 = aliased(DatasetToTag, name='dtt') + related_tag_1 = aliased(Tag, name='rt') + + related_tag_sub_query = sess.query(related_tag_1.id).filter( + related_tag_1.name.in_(related)) + + data_set_tag_join_condition = build_join_condition( + data_set_to_tag_1.dataset_id, data_set_1.id, data_set_to_tag_1.tag_id, related_tag_sub_query) + query = query.join( + data_set_to_tag_1, and_(*data_set_tag_join_condition)) + + if 'mutation' in requested or mutation: + is_outer = not bool(mutation) + mutation_join_condition = build_join_condition( + driver_result_1.mutation_id, mutation_1.id, filter_column=mutation_1.name, filter_list=mutation) + query = query.join(mutation_1, and_( + *mutation_join_condition), isouter=is_outer) + + query = build_simple_mutation_request( + query, mutation_requested, mutation_1, gene_1, mutation_type_1, entrez=entrez, mutation_code=mutation_code + ) + + return get_pagination_queries(query, paging, distinct, cursor_field=driver_result_1.id) diff --git a/apps/iatlas/api-gitlab/api/resolvers/resolver_helpers/edge.py b/apps/iatlas/api-gitlab/api/resolvers/resolver_helpers/edge.py new file mode 100644 index 0000000000..d5603d69d5 --- /dev/null +++ b/apps/iatlas/api-gitlab/api/resolvers/resolver_helpers/edge.py @@ -0,0 +1,79 @@ +from sqlalchemy import and_ +from sqlalchemy.orm import aliased +from api import db +from api.db_models import Edge, Node +from .paging_utils import get_pagination_queries +from .general_resolvers import build_join_condition, get_selected, get_value + +edge_request_fields = {'label', + 'name', + 'node1', + 'node2', + 'score'} + + +def build_edge_graphql_response(edge): + from .node import build_node_graphql_response + dict = { + 'id': get_value(edge, 'id'), + 'label': get_value(edge, 'label'), + 'name': get_value(edge, 'name'), + 'score': get_value(edge, 'score'), + 'node1': build_node_graphql_response(prefix='node1_')(edge), + 'node2': build_node_graphql_response(prefix='node2_')(edge) + } + return(dict) + + +def build_edge_request(requested, node_1_requested, node_2_requested, distinct=False, max_score=None, min_score=None, node_start=None, node_end=None, paging=None): + ''' + Builds a SQL request. + + All keyword arguments are optional. Keyword arguments are: + `max_score` - a float, a maximum score value + `min_score` - a float, a minimum score value + `node_start` - a list of strings, starting node names + `node_end` - a list of strings, ending node names + ''' + from .node import get_node_column_labels + sess = db.session + + edge_1 = aliased(Edge, name='e') + node_1 = aliased(Node, name='n1') + node_2 = aliased(Node, name='n2') + + core_field_mapping = { + 'id': edge_1.id.label('id'), + 'label': edge_1.label.label('label'), + 'name': edge_1.name.label('name'), + 'score': edge_1.score.label('score')} + + core = get_selected(requested, core_field_mapping) + node_1_core = get_node_column_labels( + node_1_requested, node_1, prefix='node1_') + node_2_core = get_node_column_labels( + node_2_requested, node_2, prefix='node2_') + + query = sess.query(*[*core, *node_1_core, *node_2_core]) + query = query.select_from(edge_1) + + if max_score != None: + query = query.filter(edge_1.score <= max_score) + + if min_score != None: + query = query.filter(edge_1.score >= min_score) + + if 'node1' in requested or node_start: + node_start_join_condition = build_join_condition( + node_1.id, edge_1.node_1_id, node_1.name, node_start) + query = query.join(node_1, and_(*node_start_join_condition)) + + if 'node2' in requested or node_end: + node_start_join_condition = build_join_condition( + node_2.id, edge_1.node_2_id, node_2.name, node_end) + query = query.join(node_2, and_(*node_start_join_condition)) + + import logging + logging.warning(query) + + return get_pagination_queries(query, paging, distinct, cursor_field=edge_1.id) diff --git a/apps/iatlas/api-gitlab/api/resolvers/resolver_helpers/feature.py b/apps/iatlas/api-gitlab/api/resolvers/resolver_helpers/feature.py new file mode 100644 index 0000000000..051bfdfd9c --- /dev/null +++ b/apps/iatlas/api-gitlab/api/resolvers/resolver_helpers/feature.py @@ -0,0 +1,430 @@ +from sqlalchemy import and_ +from sqlalchemy.orm import aliased +from api import db +from api.db_models import ( + Feature, + FeatureToSample, + Sample, + Cohort, + CohortToSample, + CohortToFeature, + SingleCellPseudobulkFeature, + CellToFeature, + CellToSample, + Cell +) +from .sample import build_sample_graphql_response +from .general_resolvers import build_join_condition, get_selected, get_value +from .paging_utils import get_pagination_queries +from decimal import Decimal + +feature_class_request_fields = {'name'} + +simple_feature_request_fields = { + 'display', + 'name', + 'order', + 'unit', + 'germlineModule', + 'germlineCategory' +} + +simple_feature_request_fields2 = { + 'display', + 'name', + 'order', + 'class' +} + +cell_feature_request_fields = simple_feature_request_fields.union({'value'}) + +cell_related_feature_request_fields = {'name', 'value'} + +feature_request_fields = simple_feature_request_fields.union({ + 'class', + 'methodTag', + 'samples', + 'pseudoBulkSamples', + 'cells', + 'valueMax', + 'valueMin' +}) + + +def build_feature_graphql_response( + requested=[], + sample_requested=[], + pseudobulk_sample_requested=[], + cell_requested=[], + max_value=None, + min_value=None, + cohort=None, + sample=None, + prefix='feature_' +): + + def f(feature): + + from .cell import build_cell_graphql_response + + if not feature: + return None + id = get_value(feature, prefix + 'id') + samples = get_samples( + [id], + requested=requested, + sample_requested=sample_requested, + max_value=max_value, + min_value=min_value, + cohort=cohort, + sample=sample + ) + pseudobulk_samples = get_pseudobulk_samples( + [id], + requested=requested, + sample_requested=pseudobulk_sample_requested, + cohort=cohort, + sample=sample + ) + cells = get_cells( + [id], + requested=requested, + cell_requested=cell_requested, + cohort=None + ) + if 'valueMax' in requested or 'valueMin' in requested: + values = [get_value(sample, 'sample_feature_value') + for sample in samples] + value_min = min(values) if 'valueMin' in requested else None + value_max = max(values) if 'valueMax' in requested else None + result = { + 'id': id, + 'class': get_value(feature, prefix + 'class'), + 'display': get_value(feature, prefix + 'display'), + 'methodTag': get_value(feature, prefix + 'method_tag'), + 'name': get_value(feature, prefix + 'name'), + 'order': get_value(feature, prefix + 'order'), + 'germlineModule': get_value(feature, prefix + 'germline_module'), + 'germlineCategory': get_value(feature, prefix + 'germline_category'), + 'unit': get_value(feature, prefix + 'unit'), + 'samples': map(build_sample_graphql_response(), samples), + 'pseudoBulkSamples': map(build_sample_graphql_response(), pseudobulk_samples), + 'cells': map(build_cell_graphql_response(), cells), + 'valueMin': value_min if type(value_min) is Decimal else None, + 'valueMax': value_max if type(value_max) is Decimal else None, + 'value': get_value(feature, prefix + 'value'), + } + + return(result) + return f + + +def build_features_query( + requested, + distinct=False, + paging=None, + feature=None, + feature_class=None, + max_value=None, + min_value=None, + sample=None, + cohort=None +): + """ + Builds a SQL request. + """ + sess = db.session + + has_min_max = 'valueMax' in requested or 'valueMin' in requested + + feature_1 = aliased(Feature, name='f') + feature_to_sample_1 = aliased(FeatureToSample, name='fts') + sample_1 = aliased(Sample, name='s') + cohort_1 = aliased(Cohort, name='c') + cohort_to_feature_1 = aliased(CohortToFeature, name='ctf') + pseudobulk_feature_1 = aliased(SingleCellPseudobulkFeature, name='scpf') + cohort_to_sample_1 = aliased(CohortToSample, name='cts') + cell_to_feature_1 = aliased(CellToFeature, name='celltofeature') + cell_to_sample_1 = aliased(CellToSample, name='celltosample') + cell_1 = aliased(Cell, name = 'cell') + + core_field_mapping = { + 'id': feature_1.id.label('feature_id'), + 'class': feature_1.feature_class.label('feature_class'), + 'display': feature_1.display.label('feature_display'), + 'methodTag': feature_1.method_tag.label('feature_method_tag'), + 'name': feature_1.name.label('feature_name'), + 'order': feature_1.order.label('feature_order'), + 'germlineModule': feature_1.germline_module.label('feature_germline_module'), + 'germlineCategory': feature_1.germline_category.label('feature_germline_category'), + 'unit': feature_1.unit.label('feature_unit') + } + + core = get_selected(requested, core_field_mapping) + core |= {feature_1.id.label('feature_id')} + + query = sess.query(*core) + query = query.select_from(feature_1) + + if feature: + query = query.filter(feature_1.name.in_(feature)) + + if feature_class: + query = query.filter(feature_1.feature_class.in_(feature_class)) + + if has_min_max or sample: + feature_to_sample_subquery = sess.query(feature_to_sample_1.feature_id) + + if max_value: + feature_to_sample_subquery = feature_to_sample_subquery.filter( + feature_to_sample_1.feature_to_sample_value <= max_value) + + if min_value: + feature_to_sample_subquery = feature_to_sample_subquery.filter( + feature_to_sample_1.feature_to_sample_value >= min_value) + + if sample: + + sample_join_condition = build_join_condition( + feature_to_sample_1.sample_id, sample_1.id, filter_column=sample_1.name, filter_list=sample) + cohort_subquery = feature_to_sample_subquery.join(sample_1, and_( + *sample_join_condition), isouter=False) + + feature_to_sample_subquery = feature_to_sample_subquery.filter( + sample_1.name.in_(sample)) + + query = query.filter(feature_1.id.in_(feature_to_sample_subquery)) + + + if cohort: + + + cohort_subquery1 = sess.query(pseudobulk_feature_1.feature_id) + + cohort_to_sample_join_condition1 = build_join_condition( + pseudobulk_feature_1.sample_id, cohort_to_sample_1.sample_id + ) + cohort_subquery1 = cohort_subquery1.join(cohort_to_sample_1,and_( + *cohort_to_sample_join_condition1), isouter=False + ) + + cohort_join_condition1 = build_join_condition( + cohort_to_sample_1.cohort_id, cohort_1.id, filter_column=cohort_1.name, filter_list=cohort + ) + cohort_subquery1 = cohort_subquery1.join(cohort_1,and_( + *cohort_join_condition1), isouter=False + ) + + cohort_subquery2 = sess.query(cohort_to_feature_1.feature_id) + + cohort_join_condition2 = build_join_condition( + cohort_to_feature_1.cohort_id, cohort_1.id, filter_column=cohort_1.name, filter_list=cohort) + cohort_subquery2 = cohort_subquery2.join(cohort_1, and_( + *cohort_join_condition2), isouter=False) + + union_subquery = cohort_subquery1.union(cohort_subquery2) + + query = query.filter(feature_1.id.in_(union_subquery)) + + return get_pagination_queries(query, paging, distinct, cursor_field=feature_1.id) + + +def get_samples(feature_id, requested, sample_requested, max_value=None, min_value=None, cohort=None, sample=None): + has_samples = 'samples' in requested + has_max_min = 'valueMax' in requested or 'valueMin' in requested + + if (has_samples or has_max_min): + + sess = db.session + + feature_to_sample_1 = aliased(FeatureToSample, name='fts') + sample_1 = aliased(Sample, name='s') + cohort_1 = aliased(Cohort, name='c') + cohort_to_sample_1 = aliased(CohortToSample, name='cts') + + sample_core_field_mapping = { + 'name': sample_1.name.label('sample_name')} + + sample_core = get_selected(sample_requested, sample_core_field_mapping) + # Always select the sample id and the feature id. + sample_core |= {sample_1.id.label( + 'id'), feature_to_sample_1.feature_id.label('feature_id')} + + if has_max_min or 'value' in sample_requested: + sample_core |= {feature_to_sample_1.feature_to_sample_value.label( + 'sample_feature_value')} + + sample_query = sess.query(*sample_core) + sample_query = sample_query.select_from(sample_1) + + if sample: + sample_query = sample_query.filter(sample_1.name.in_(sample)) + + feature_sample_join_condition = build_join_condition( + feature_to_sample_1.sample_id, sample_1.id, feature_to_sample_1.feature_id, feature_id) + + if max_value: + feature_sample_join_condition.append( + feature_to_sample_1.feature_to_sample_value <= max_value) + + if min_value: + feature_sample_join_condition.append( + feature_to_sample_1.feature_to_sample_value >= min_value) + + sample_query = sample_query.join( + feature_to_sample_1, and_(*feature_sample_join_condition)) + + if cohort: + cohort_subquery = sess.query(cohort_to_sample_1.sample_id) + + cohort_join_condition = build_join_condition( + cohort_to_sample_1.cohort_id, cohort_1.id, filter_column=cohort_1.name, filter_list=cohort) + cohort_subquery = cohort_subquery.join(cohort_1, and_( + *cohort_join_condition), isouter=False) + + sample_query = sample_query.filter( + sample_1.id.in_(cohort_subquery)) + + samples = sample_query.distinct().all() + return samples + + return [] + + +def get_pseudobulk_samples(feature_id, requested, sample_requested, cohort=None, sample=None): + + + if 'pseudoBulkSamples' not in requested: + return [] + + sess = db.session + + sample_1 = aliased(Sample, name='s') + cohort_1 = aliased(Cohort, name='c') + cohort_to_sample_1 = aliased(CohortToSample, name='cts') + pseudobulk_feature_1 = aliased(SingleCellPseudobulkFeature, name='scpf') + + sample_core_field_mapping = { + 'name': sample_1.name.label('sample_name')} + + sample_core = get_selected(sample_requested, sample_core_field_mapping) + + sample_core |= { + sample_1.id.label('sample_id'), + pseudobulk_feature_1.feature_id.label('sample_feature_id') + } + + if 'value' in sample_requested: + sample_core |= { + pseudobulk_feature_1.value.label('sample_feature_value') + } + + if 'cellType' in sample_requested: + sample_core |= { + pseudobulk_feature_1.cell_type.label('sample_cell_type') + } + + query = sess.query(*sample_core) + query = query.select_from(sample_1) + + sample_join_condition = build_join_condition( + pseudobulk_feature_1.sample_id, + sample_1.id, + pseudobulk_feature_1.feature_id, + feature_id + ) + query = query.join( + pseudobulk_feature_1, and_(*sample_join_condition)) + + + if sample: + query = query.filter(sample_1.name.in_(sample)) + + if cohort: + cohort_subquery = sess.query(pseudobulk_feature_1.feature_id) + + cohort_to_sample_join_condition = build_join_condition( + pseudobulk_feature_1.sample_id, cohort_to_sample_1.sample_id + ) + cohort_subquery = cohort_subquery.join(cohort_to_sample_1,and_( + *cohort_to_sample_join_condition), isouter=False + ) + + cohort_join_condition = build_join_condition( + cohort_to_sample_1.cohort_id, cohort_1.id, filter_column=cohort_1.name, filter_list=cohort + ) + cohort_subquery = cohort_subquery.join(cohort_1,and_( + *cohort_join_condition), isouter=False + ) + + query = query.filter(pseudobulk_feature_1.feature_id.in_(cohort_subquery)) + + samples = query.distinct().all() + return samples + + +def get_cells(feature_id, requested, cell_requested, cohort=None): + + if 'cells' not in requested: + return [] + + sess = db.session + + cell_1 = aliased(Cell, name='c') + cell_to_feature_1 = aliased(CellToFeature, name='ctf') + cohort_1 = aliased(Cohort, name='c') + cohort_to_sample_1 = aliased(CohortToSample, name='cts') + cell_to_sample_1 = aliased(CellToSample, name='celltosample') + cell_to_feature_1 = aliased(CellToFeature, name='celltofeature') + cell_1 = aliased(Cell, name = 'cell') + + cell_core_field_mapping = { + 'name': cell_1.name.label('cell_name') + } + + cell_core = get_selected(cell_requested, cell_core_field_mapping) + + cell_core |= { + cell_1.id.label('cell_id') + } + + if 'value' in cell_requested: + cell_core |= { + cell_to_feature_1.feature_value.label('cell_feature_value') + } + + if 'type' in cell_requested: + cell_core |= { + cell_1.cell_type.label('cell_type') + } + + query = sess.query(*cell_core) + query = query.select_from(cell_1) + + cell_to_feature_join_condition = build_join_condition( + cell_1.id, + cell_to_feature_1.cell_id, + cell_to_feature_1.feature_id, + feature_id + ) + query = query.join( + cell_to_feature_1, and_(*cell_to_feature_join_condition)) + + if cohort: + + cohort_id_subquery = sess.query(cohort_1.id) + cohort_id_subquery = cohort_id_subquery.filter(cohort_1.name.in_(cohort)) + + sample_id_subquery = sess.query(cohort_to_sample_1.sample_id) + sample_id_subquery = sample_id_subquery.filter(cohort_to_sample_1.cohort_id.in_(cohort_id_subquery)) + + cell_id_subquery = sess.query(cell_to_sample_1.cell_id) + cell_id_subquery = cell_id_subquery.filter(cell_to_sample_1.sample_id.in_(sample_id_subquery)) + + feature_id_subquery = sess.query(cell_to_feature_1.feature_id) + feature_id_subquery = feature_id_subquery.filter(cell_to_feature_1.cell_id.in_(cell_id_subquery)) + + query = query.filter(feature_1.id.in_(feature_id_subquery)) + + cells = query.distinct().all() + return cells diff --git a/apps/iatlas/api-gitlab/api/resolvers/resolver_helpers/gene.py b/apps/iatlas/api-gitlab/api/resolvers/resolver_helpers/gene.py new file mode 100644 index 0000000000..e64cc10754 --- /dev/null +++ b/apps/iatlas/api-gitlab/api/resolvers/resolver_helpers/gene.py @@ -0,0 +1,695 @@ +from sqlalchemy import and_ +from sqlalchemy.orm import aliased +from itertools import groupby +from api import db +from api.db_models import ( + Cohort, + CohortToSample, + CohortToGene, + Gene, + GeneToSample, + GeneToGeneSet, + GeneSet, + Publication, + PublicationToGeneToGeneSet, + Sample, + SingleCellPseudobulk, + CellToSample, + CellToGene, + Cell +) +from .general_resolvers import build_join_condition, get_selected, get_value +from .publication import build_publication_graphql_response +from .paging_utils import get_pagination_queries, fetch_page +from .sample import build_sample_graphql_response, build_gene_expression_graphql_response, build_single_cell_seq_response + + +simple_gene_request_fields = { + 'entrez', + 'hgnc', + 'description', + 'friendlyName', + 'ioLandscapeName' +} + +cell_gene_request_fields = simple_gene_request_fields.union({'singleCellSeq'}) + +gene_request_fields = simple_gene_request_fields.union({ + 'geneFamily', + 'geneFunction', + 'geneTypes', + 'immuneCheckpoint', + 'pathway', + 'publications', + 'samples', + 'pseudoBulkSamples', + 'superCategory', + 'therapyType', + 'cells' +}) + + +def get_simple_gene_column_labels(requested, gene): + mapping = { + 'entrez': gene.entrez_id.label('gene_entrez'), + 'hgnc': gene.hgnc_id.label('gene_hgnc'), + 'description': gene.description.label('gene_description'), + 'friendlyName': gene.friendly_name.label('gene_friendly_name'), + 'ioLandscapeName': gene.io_landscape_name.label('gene_io_landscape_name') + } + labels = get_selected(requested, mapping) + return(labels) + + +def build_gene_graphql_response( + requested=[], + gene_types_requested=[], + publications_requested=[], + sample_requested=[], + pseudobulk_sample_requested=[], + cell_requested=[], + gene_type=None, + cohort=None, + sample=None, + max_rna_seq_expr=None, + min_rna_seq_expr=None, + prefix='gene_' +): + + def f(gene): + from .cell import build_cell_graphql_response + + if not gene: + return None + + id = get_value(gene, prefix + 'id') + gene_types = get_gene_types( + id, requested, gene_types_requested, gene_type=gene_type) + publications = get_publications(id, requested, publications_requested) + samples = get_samples( + id, + requested, + sample_requested, + cohort, + sample, + max_rna_seq_expr, + min_rna_seq_expr + ) + pseudobulk_samples = get_pseudobulk_samples( + [id], + requested=requested, + sample_requested=pseudobulk_sample_requested, + cohort=cohort, + sample=sample + ) + cells = get_cells( + [id], + requested=requested, + cell_requested=cell_requested, + cohort=None + ) + result_dict = { + 'id': id, + 'entrez': get_value(gene, prefix + 'entrez') or get_value(gene, prefix + 'entrez_id'), + 'hgnc': get_value(gene, prefix + 'hgnc') or get_value(gene, prefix + 'hgnc_id'), + 'description': get_value(gene, prefix + 'description'), + 'friendlyName': get_value(gene, prefix + 'friendly_name'), + 'ioLandscapeName': get_value(gene, prefix + 'io_landscape_name'), + 'geneFamily': get_value(gene, prefix + 'family'), + 'geneFunction': get_value(gene, prefix + 'function'), + 'immuneCheckpoint': get_value(gene, prefix + 'immune_checkpoint'), + 'pathway': get_value(gene, prefix + 'pathway'), + 'superCategory': get_value(gene, prefix + 'super_category'), + 'therapyType': get_value(gene, prefix + 'therapy_type'), + 'geneTypes': gene_types, + 'publications': map(build_publication_graphql_response, publications), + 'samples': map(build_gene_expression_graphql_response(), samples), + 'pseudoBulkSamples': map(build_single_cell_seq_response(), pseudobulk_samples), + 'cells': map(build_cell_graphql_response(), cells), + } + return result_dict + return f + + +def build_pub_gene_gene_type_join_condition(gene_ids, gene_type, pub_gene_gene_type_model, pub_model): + join_condition = build_join_condition( + pub_gene_gene_type_model.publication_id, pub_model.id, pub_gene_gene_type_model.gene_id, gene_ids) + + if gene_type: + gene_type_1 = aliased(GeneSet, name='gt') + gene_type_subquery = db.session.query(gene_type_1.id).filter( + gene_type_1.name.in_(gene_type)) + join_condition.append( + pub_gene_gene_type_model.gene_type_id.in_(gene_type_subquery)) + + return join_condition + + +def build_gene_request( + requested, + distinct=False, + paging=None, + entrez=None, + gene_family=None, + gene_function=None, + gene_type=None, + immune_checkpoint=None, + pathway=None, + super_category=None, + therapy_type=None, + cohort=None, + sample=None, + max_rna_seq_expr=None, + min_rna_seq_expr=None +): + ''' + Builds a SQL request. + + All positional arguments are required. Positional arguments are: + 1st position - a set of the requested fields at the root of the graphql request + 2nd position - a set of the requested fields in the 'tag' node of the graphql request. If 'tag' is not requested, this will be an empty set. + + All keyword arguments are optional. Keyword arguments are: + `data_set` - a list of strings, data set names + `entrez` - a list of integers, gene entrez ids + `gene_family` - a list of strings, gene family names + `gene_function` - a list of strings, gene function names + `gene_type` - a list of strings, gene type names + `immune_checkpoint` - a list of strings, immune checkpoint names + `max_rna_seq_expr` - a float, a maximum RNA Sequence Expression value + `min_rna_seq_expr` - a float, a minimum RNA Sequence Expression value + `pathway` - a list of strings, pathway names + 'paging' - an instance of PagingInput + `type` - a string, the type of pagination to perform. Must be either 'OFFSET' or 'CURSOR'." + `page` - an integer, when performing OFFSET paging, the page number requested. + `limit` - an integer, when performing OFFSET paging, the number or records requested. + `first` - an integer, when performing CURSOR paging, the number of records requested AFTER the CURSOR. + `last` - an integer, when performing CURSOR paging, the number of records requested BEFORE the CURSOR. + `before` - an integer, when performing CURSOR paging: the CURSOR to be used in tandem with 'last' + `after` - an integer, when performing CURSOR paging: the CURSOR to be used in tandem with 'first' + `related` - a list of strings, tag names related to data sets + `sample` - a list of strings, sample names + `super_category` - a list of strings, super category names + `tag` - a list of strings, tag names related to samples + `therapy_type` - a list of strings, therapy type names + ''' + sess = db.session + + gene_1 = aliased(Gene, name='g') + gene_to_sample_1 = aliased(GeneToSample, name='gts') + gene_to_type_1 = aliased(GeneToGeneSet, name='ggt') + gene_type_1 = aliased(GeneSet, name='gt') + sample_1 = aliased(Sample, name='s') + cohort_1 = aliased(Cohort, name='c') + cohort_to_gene_1 = aliased(CohortToGene, name='ctg') + pseudobulk_1 = aliased(SingleCellPseudobulk, name='scp') + cohort_to_sample_1 = aliased(CohortToSample, name='cts') + cell_to_sample_1 = aliased(CellToSample, name='celltosample') + cell_to_gene_1 = aliased(CellToGene, name='celltogene') + + core_field_mapping = { + 'id': gene_1.id.label('gene_id'), + 'entrez': gene_1.entrez_id.label('gene_entrez'), + 'hgnc': gene_1.hgnc_id.label('gene_hgnc'), + 'description': gene_1.description.label('gene_description'), + 'friendlyName': gene_1.friendly_name.label('gene_friendly_name'), + 'ioLandscapeName': gene_1.io_landscape_name.label('gene_io_landscape_name'), + 'geneFamily': gene_1.gene_family.label('gene_family'), + 'geneFunction': gene_1.gene_function.label('gene_function'), + 'immuneCheckpoint': gene_1.immune_checkpoint.label('gene_immune_checkpoint'), + 'pathway': gene_1.gene_pathway.label('gene_pathway'), + 'superCategory': gene_1.super_category.label('gene_super_category'), + 'therapyType': gene_1.therapy_type.label('gene_therapy_type') + } + + core = get_selected(requested, core_field_mapping) + core |= {gene_1.id.label('gene_id')} + + query = sess.query(*core) + query = query.select_from(gene_1) + + if entrez: + query = query.filter(gene_1.entrez_id.in_(entrez)) + + if gene_type: + query = query.join( + gene_to_type_1, and_( + gene_to_type_1.gene_id == gene_1.id, gene_to_type_1.gene_set_id.in_( + sess.query(gene_type_1.id).filter( + gene_type_1.name.in_(gene_type)) + ) + ) + ) + + if gene_family: + query = query.filter(gene_1.gene_family.in_(gene_family)) + + if gene_function: + query = query.filter(gene_1.gene_function.in_(gene_function)) + + if immune_checkpoint: + query = query.filter(gene_1.immune_checkpoint.in_(immune_checkpoint)) + + if pathway: + query = query.filter(gene_1.pathway.in_(pathway)) + + if super_category: + query = query.filter(gene_1.super_category.in_(super_category)) + + if therapy_type: + query = query.filter(gene_1.therapy_type.in_(therapy_type)) + + if max_rna_seq_expr or min_rna_seq_expr or sample: + gene_to_sample_subquery = sess.query(gene_to_sample_1.gene_id) + + if max_rna_seq_expr: + gene_to_sample_subquery = gene_to_sample_subquery.filter( + gene_to_sample_1.rna_seq_expression <= max_rna_seq_expr) + + if min_rna_seq_expr: + gene_to_sample_subquery = gene_to_sample_subquery.filter( + gene_to_sample_1.rna_seq_expression >= min_rna_seq_expr) + + if sample: + + sample_join_condition = build_join_condition( + gene_to_sample_1.sample_id, sample_1.id, filter_column=sample_1.name, filter_list=sample) + gene_to_sample_subquery = gene_to_sample_subquery.join(sample_1, and_( + *sample_join_condition), isouter=False) + + gene_to_sample_subquery = gene_to_sample_subquery.filter( + sample_1.name.in_(sample)) + + query = query.filter(gene_1.id.in_(gene_to_sample_subquery)) + + + if cohort: + + cohort_subquery1 = sess.query(pseudobulk_1.gene_id) + + cohort_to_sample_join_condition1 = build_join_condition( + pseudobulk_1.sample_id, cohort_to_sample_1.sample_id + ) + cohort_subquery1 = cohort_subquery1.join(cohort_to_sample_1,and_( + *cohort_to_sample_join_condition1), isouter=False + ) + + cohort_join_condition1 = build_join_condition( + cohort_to_sample_1.cohort_id, cohort_1.id, filter_column=cohort_1.name, filter_list=cohort + ) + cohort_subquery1 = cohort_subquery1.join(cohort_1,and_( + *cohort_join_condition1), isouter=False + ) + + + cohort_subquery2 = sess.query(cohort_to_gene_1.gene_id) + + cohort_join_condition2 = build_join_condition( + cohort_to_gene_1.cohort_id, cohort_1.id, filter_column=cohort_1.name, filter_list=cohort) + cohort_subquery2 = cohort_subquery2.join(cohort_1, and_( + *cohort_join_condition2), isouter=False) + + + cohort_id_subquery = sess.query(cohort_1.id) + cohort_id_subquery = cohort_id_subquery.filter(cohort_1.name.in_(cohort)) + + sample_id_subquery = sess.query(cohort_to_sample_1.sample_id) + sample_id_subquery = sample_id_subquery.filter(cohort_to_sample_1.cohort_id.in_(cohort_id_subquery)) + + cell_id_subquery = sess.query(cell_to_sample_1.cell_id) + cell_id_subquery = cell_id_subquery.filter(cell_to_sample_1.sample_id.in_(sample_id_subquery)) + + gene_id_subquery3 = sess.query(cell_to_gene_1.gene_id) + gene_id_subquery3 = gene_id_subquery3.filter(cell_to_gene_1.cell_id.in_(cell_id_subquery)) + + query = query.filter(gene_1.id.in_(cohort_subquery1) | gene_1.id.in_(cohort_subquery2) | gene_1.id.in_(gene_id_subquery3)) + + return get_pagination_queries(query, paging, distinct, cursor_field=gene_1.id) + + +def get_samples(id, requested, sample_requested, cohort=None, sample=None, max_rna_seq_expr=None, min_rna_seq_expr=None): + + if 'samples' not in requested: + return [] + + sess = db.session + + gene_to_sample_1 = aliased(GeneToSample, name='fts') + sample_1 = aliased(Sample, name='s') + cohort_1 = aliased(Cohort, name='c') + cohort_to_sample_1 = aliased(CohortToSample, name='cts') + + core_field_mapping = { + 'name': sample_1.name.label('sample_name'), + 'rnaSeqExpr': gene_to_sample_1.rna_seq_expression.label('sample_gene_rna_seq_expr'), + 'nanostringExpr': gene_to_sample_1.nanostring_expression.label('sample_gene_nanostring_expr') + } + + core = get_selected(sample_requested, core_field_mapping) + + core |= { + sample_1.id.label('sample_id'), + gene_to_sample_1.gene_id.label('gene_id'), + } + + query = sess.query(*core) + query = query.select_from(sample_1) + + if sample: + query = query.filter(sample_1.name.in_(sample)) + + gene_sample_join_condition = build_join_condition( + gene_to_sample_1.sample_id, sample_1.id, gene_to_sample_1.gene_id, [id]) + + if max_rna_seq_expr: + query = query.filter( + gene_to_sample_1.rna_seq_expression <= max_rna_seq_expr) + + if min_rna_seq_expr: + query = query.filter( + gene_to_sample_1.rna_seq_expression >= min_rna_seq_expr) + + query = query.join( + gene_to_sample_1, and_(*gene_sample_join_condition)) + + if cohort: + cohort_subquery = sess.query(cohort_to_sample_1.sample_id) + + cohort_join_condition = build_join_condition( + cohort_to_sample_1.cohort_id, cohort_1.id, filter_column=cohort_1.name, filter_list=cohort) + cohort_subquery = cohort_subquery.join(cohort_1, and_( + *cohort_join_condition), isouter=False) + + query = query.filter( + sample_1.id.in_(cohort_subquery)) + + samples = query.distinct().all() + return samples + +def get_cell_type_samples(gene_id, requested, cell_type_sample_requested, cohort=None, sample=None): + + + if 'cellTypeSamples' not in requested: + return [] + + sess = db.session + + sample_1 = aliased(Sample, name='s') + cohort_1 = aliased(Cohort, name='c') + cohort_to_sample_1 = aliased(CohortToSample, name='cts') + pseudobulk_1 = aliased(SingleCellPseudobulk, name='scp') + + sample_core_field_mapping = { + 'name': sample_1.name.label('sample_name')} + + sample_core = get_selected(cell_type_sample_requested, sample_core_field_mapping) + + sample_core |= { + sample_1.id.label('sample_id'), + pseudobulk_1.gene_id.label('sample_gene_id') + } + + if 'singleCellSeqSum' in cell_type_sample_requested: + sample_core |= { + pseudobulk_1.single_cell_seq_sum.label('sample_single_cell_seq_sum') + } + + if 'cellType' in cell_type_sample_requested: + sample_core |= { + pseudobulk_1.cell_type.label('sample_cell_type') + } + + query = sess.query(*sample_core) + query = query.select_from(sample_1) + + query = query.filter(pseudobulk_1.gene_id.in_([gene_id])) + + sample_join_condition = build_join_condition( + pseudobulk_1.sample_id, + sample_1.id, + ) + query = query.join( + pseudobulk_1, and_(*sample_join_condition)) + + if sample: + query = query.filter(sample_1.name.in_(sample)) + + if cohort: + cohort_subquery = sess.query(pseudobulk_1.gene_id) + + cohort_to_sample_join_condition = build_join_condition( + pseudobulk_1.sample_id, cohort_to_sample_1.sample_id + ) + cohort_subquery = cohort_subquery.join(cohort_to_sample_1,and_( + *cohort_to_sample_join_condition), isouter=False + ) + + cohort_join_condition = build_join_condition( + cohort_to_sample_1.cohort_id, cohort_1.id, filter_column=cohort_1.name, filter_list=cohort + ) + cohort_subquery = cohort_subquery.join(cohort_1,and_( + *cohort_join_condition), isouter=False + ) + + query = query.filter(pseudobulk_1.gene_id.in_(cohort_subquery)) + + + samples = query.distinct().all() + return samples + + + +def get_gene_types(gene_id, requested, gene_types_requested, gene_type=None): + + if 'geneTypes' not in requested: + return None + + sess = db.session + + gene_type_1 = aliased(GeneSet, name='gt') + gene_to_gene_type_1 = aliased(GeneToGeneSet, name='ggt') + + core_field_mapping = { + 'name': gene_type_1.name.label('name'), + 'display': gene_type_1.display.label('display') + } + + core = get_selected(gene_types_requested, core_field_mapping) + core |= { + gene_type_1.id.label('gene_id'), + gene_to_gene_type_1.gene_id.label('gene_type_id') + } + + gene_type_query = sess.query(*core) + gene_type_query = gene_type_query.select_from(gene_type_1) + + gene_gene_type_join_condition = build_join_condition( + gene_to_gene_type_1.gene_set_id, gene_type_1.id, gene_to_gene_type_1.gene_id, [gene_id]) + + if gene_type: + gene_gene_type_join_condition.append( + gene_type_1.name.in_(gene_type)) + + gene_type_query = gene_type_query.join(gene_to_gene_type_1, and_( + *gene_gene_type_join_condition)) + + order = [] + append_to_order = order.append + if 'name' in gene_types_requested: + append_to_order(gene_type_1.name) + if 'display' in gene_types_requested: + append_to_order(gene_type_1.display) + if not order: + append_to_order(gene_type_1.id) + gene_type_query = gene_type_query.order_by(*order) + + return gene_type_query.distinct().all() + + +def get_publications(gene_id, requested, publications_requested): + + if 'publications' not in requested: + return [] + + sess = db.session + + pub_1 = aliased(Publication, name='p') + pub_gene_gene_type_1 = aliased(PublicationToGeneToGeneSet, name='pggt') + + core_field_mapping = { + 'doId': pub_1.do_id.label('do_id'), + 'firstAuthorLastName': pub_1.first_author_last_name.label('first_author_last_name'), + 'journal': pub_1.journal.label('journal'), + 'name': pub_1.title.label('name'), + 'pubmedId': pub_1.pubmed_id.label('pubmed_id'), + 'title': pub_1.title.label('title'), + 'year': pub_1.year.label('year') + } + + core = get_selected(publications_requested, core_field_mapping) + core.add(pub_gene_gene_type_1.gene_id.label('gene_id')) + + query = sess.query(*core) + query = query.select_from(pub_1) + + ptgtgt_join_condition = build_join_condition( + pub_1.id, pub_gene_gene_type_1.publication_id, filter_column=pub_gene_gene_type_1.gene_id, filter_list=[gene_id]) + query = query.join(pub_gene_gene_type_1, and_( + *ptgtgt_join_condition), isouter=False) + + order = [] + append_to_order = order.append + if 'name' in publications_requested: + append_to_order(pub_1.name) + if 'pubmedId' in publications_requested: + append_to_order(pub_1.pubmed_id) + if 'doId' in publications_requested: + append_to_order(pub_1.do_id) + if 'title' in publications_requested: + append_to_order(pub_1.title) + if 'firstAuthorLastName' in publications_requested: + append_to_order(pub_1.first_author_last_name) + if 'year' in publications_requested: + append_to_order(pub_1.year) + if 'journal' in publications_requested: + append_to_order(pub_1.journal) + query = query.order_by(*order) if order else query + + return query.distinct().all() + + +def get_pseudobulk_samples(gene_id, requested, sample_requested, cohort=None, sample=None): + + if 'pseudoBulkSamples' not in requested: + return [] + + sess = db.session + + sample_1 = aliased(Sample, name='s') + cohort_1 = aliased(Cohort, name='c') + cohort_to_sample_1 = aliased(CohortToSample, name='cts') + pseudobulk_1 = aliased(SingleCellPseudobulk, name='scp') + + sample_core_field_mapping = { + 'name': sample_1.name.label('sample_name')} + + sample_core = get_selected(sample_requested, sample_core_field_mapping) + + sample_core |= { + sample_1.id.label('sample_id'), + pseudobulk_1.gene_id.label('sample_gene_id') + } + + if 'singleCellSeqSum' in sample_requested: + sample_core |= { + pseudobulk_1.single_cell_seq_sum.label('sample_single_cell_seq_sum') + } + + if 'cellType' in sample_requested: + sample_core |= { + pseudobulk_1.cell_type.label('sample_cell_type') + } + + query = sess.query(*sample_core) + query = query.select_from(sample_1) + + sample_join_condition = build_join_condition( + pseudobulk_1.sample_id, + sample_1.id, + pseudobulk_1.gene_id, + gene_id + ) + query = query.join( + pseudobulk_1, and_(*sample_join_condition)) + + + if sample: + query = query.filter(sample_1.name.in_(sample)) + + if cohort: + cohort_subquery = sess.query(pseudobulk_1.gene_id) + + cohort_to_sample_join_condition = build_join_condition( + pseudobulk_1.sample_id, cohort_to_sample_1.sample_id + ) + cohort_subquery = cohort_subquery.join(cohort_to_sample_1,and_( + *cohort_to_sample_join_condition), isouter=False + ) + + cohort_join_condition = build_join_condition( + cohort_to_sample_1.cohort_id, cohort_1.id, filter_column=cohort_1.name, filter_list=cohort + ) + cohort_subquery = cohort_subquery.join(cohort_1,and_( + *cohort_join_condition), isouter=False + ) + + query = query.filter(pseudobulk_1.gene_id.in_(cohort_subquery)) + + samples = query.distinct().all() + return samples + + +def get_cells(gene_id, requested, cell_requested, cohort=None): + + if 'cells' not in requested: + return [] + + sess = db.session + + cell_1 = aliased(Cell, name='c') + cell_to_gene_1 = aliased(CellToGene, name='ctg') + cohort_1 = aliased(Cohort, name='c') + cohort_to_sample_1 = aliased(CohortToSample, name='cts') + cell_to_sample_1 = aliased(CellToSample, name='celltosample') + cell_1 = aliased(Cell, name = 'cell') + + cell_core_field_mapping = { + 'name': cell_1.name.label('cell_name') + } + + cell_core = get_selected(cell_requested, cell_core_field_mapping) + + cell_core |= { + cell_1.id.label('cell_id') + } + + if 'singleCellSeq' in cell_requested: + cell_core |= { + cell_to_gene_1.single_cell_seq.label('cell_single_cell_seq') + } + + if 'type' in cell_requested: + cell_core |= { + cell_1.cell_type.label('cell_type') + } + + query = sess.query(*cell_core) + query = query.select_from(cell_1) + + cell_to_gene_join_condition = build_join_condition( + cell_1.id, + cell_to_gene_1.cell_id, + cell_to_gene_1.gene_id, + gene_id + ) + query = query.join( + cell_to_gene_1, and_(*cell_to_gene_join_condition)) + + if cohort: + + cohort_id_subquery = sess.query(cohort_1.id) + cohort_id_subquery = cohort_id_subquery.filter(cohort_1.name.in_(cohort)) + + sample_id_subquery = sess.query(cohort_to_sample_1.sample_id) + sample_id_subquery = sample_id_subquery.filter(cohort_to_sample_1.cohort_id.in_(cohort_id_subquery)) + + cell_id_subquery = sess.query(cell_to_sample_1.cell_id) + cell_id_subquery = cell_id_subquery.filter(cell_to_sample_1.sample_id.in_(sample_id_subquery)) + + gene_id_subquery = sess.query(cell_to_gene_1.gene_id) + gene_id_subquery = gene_id_subquery.filter(cell_to_gene_1.cell_id.in_(cell_id_subquery)) + + query = query.filter(gene_1.id.in_(gene_id_subquery)) + + cells = query.distinct().all() + return cells \ No newline at end of file diff --git a/apps/iatlas/api-gitlab/api/resolvers/resolver_helpers/gene_set.py b/apps/iatlas/api-gitlab/api/resolvers/resolver_helpers/gene_set.py new file mode 100644 index 0000000000..7268cf0aec --- /dev/null +++ b/apps/iatlas/api-gitlab/api/resolvers/resolver_helpers/gene_set.py @@ -0,0 +1,79 @@ +from sqlalchemy import orm +from api import db +from api.db_models import Gene, GeneSet +from .general_resolvers import build_option_args, get_selection_set + +simple_gene_set_request_fields = {'display', 'name'} + +gene_set_request_fields = simple_gene_set_request_fields.union({'genes'}) + + +def build_gene_set_core_request(selection_set, name=None): + """ + Builds a SQL request with just core gene set fields. + """ + sess = db.session + + gene_set_1 = orm.aliased(GeneSet, name='g') + + core_field_mapping = {'display': gene_set_1.display.label('display'), + 'name': gene_set_1.name.label('name')} + + core = build_option_args(selection_set, core_field_mapping) + + requested = build_option_args( + selection_set, {'display': 'display', 'name': 'name'}) + + query = sess.query(*core) + + if name: + query = query.filter(gene_set_1.name.in_(name)) + + order = [] + append_to_order = order.append + if 'name' in requested: + append_to_order(gene_set_1.name) + if 'display' in requested: + append_to_order(gene_set_1.display) + query = query.order_by(*order) if order else query + + return query + + +def build_gene_set_request(_obj, info, name=None): + """ + Builds a SQL request. + """ + sess = db.session + + selection_set = get_selection_set(info=info) + + gene_1 = orm.aliased(Gene, name='g') + gene_set_1 = orm.aliased(GeneSet, name='m') + + related_field_mapping = {'genes': 'genes'} + + relations = build_option_args(selection_set, related_field_mapping) + option_args = [] + + query = sess.query(gene_set_1) + + if name: + query = query.filter(gene_set_1.name.in_(name)) + + if 'genes' in relations: + query = query.join((gene_1, gene_set_1.genes), isouter=True) + option_args.append(orm.contains_eager( + gene_set_1.genes.of_type(gene_1))) + + query = query.order_by(gene_set_1.name, gene_set_1.display) + + if option_args: + return query.options(*option_args) + + return build_gene_set_core_request(selection_set, name) + + +def request_gene_sets(_obj, info, name=None): + query = build_gene_set_request(_obj, info, name=name) + return query.distinct().all() diff --git a/apps/iatlas/api-gitlab/api/resolvers/resolver_helpers/general_resolvers.py b/apps/iatlas/api-gitlab/api/resolvers/resolver_helpers/general_resolvers.py new file mode 100644 index 0000000000..2bbb0d3e42 --- /dev/null +++ b/apps/iatlas/api-gitlab/api/resolvers/resolver_helpers/general_resolvers.py @@ -0,0 +1,77 @@ + +def build_join_condition(join_column, column, filter_column=None, filter_list=None): + ''' + A helper function that creates a list of conditions. + + All positional arguments are required. TheyPositional arguments are: + 1st position - a column from the joining table or value who's value is to be compared as equal to the value passed in 2nd position + 2nd position - a column or value who's value is to be compared as equal to the value passed in ist position + + ie: join_column == column + + All keyword arguments are optional. Keyword arguments are: + `filter_column` - the column that will have the `in_` filter applied to it. This will only be applied if an actual value is passed to `filter_list`. + `filter_list` - A list of values to be applied to the `in_` filter. This may be a list of values or a subquery that returns a list. + ''' + join_condition = [join_column == column] + if bool(filter_list): + join_condition.append(filter_column.in_(filter_list)) + return join_condition + + +def build_option_args(selection_set=None, valid_nodes={}): + option_args = set() + add_to_option_args = option_args.add + if selection_set: + for selection in selection_set.selections: + if selection.name.value in valid_nodes: + if isinstance(valid_nodes, set): + add_to_option_args(selection.name.value) + else: + add_to_option_args(valid_nodes.get(selection.name.value)) + return option_args + + +def get_requested(info=None, requested_field_mapping={}, child_node=None, selection_set=[]): + selection_set = get_selection_set(selection_set, child_node, info) + return build_option_args(selection_set, requested_field_mapping) + + +def get_selected(requested, selected_field_mapping): + selected_keys = set([*selected_field_mapping]).intersection(requested) + return set(map(selected_field_mapping.get, selected_keys)) + + +def get_selection_set(selection_set=[], child_node=None, info=None): + ''' + A helper function to get the selection set from a graphql request. + + All keyword arguments are optional. Keyword arguments are: + `selection_set` - an initial set to get the selection set from. Defaults to an empty list. + `child_node` - the node one level down to look in. If this has no value, the root of the set it used. Defaults to None. + `info` - the info object from a request. If this is passed, the selection set will be taken from here and any passed selection set will be ignored. Defaults to None + ''' + selection_set = info.field_nodes[0].selection_set if info else selection_set + if selection_set and child_node: + new_selection_set = [] + for selection in selection_set.selections: + if selection.name.value == child_node: + new_selection_set = selection.selection_set + break + return new_selection_set + + return selection_set + + +def get_value(obj=None, attribute='name', default=None): + ''' + A helper function to get attribute values from an object. + + All keyword arguments are optional. Keyword arguments are: + `obj` - the object to get the value from. If no object is passed or the value is None, the default will be returned. Defaults to None. + `attribute` - the attribute name to look for. Defaults to 'name'. + `default` - the default value to return if the attribute is not found. Defaults to None + ''' + if obj: + return getattr(obj, attribute, default) + return default diff --git a/apps/iatlas/api-gitlab/api/resolvers/resolver_helpers/germline_gwas_result.py b/apps/iatlas/api-gitlab/api/resolvers/resolver_helpers/germline_gwas_result.py new file mode 100644 index 0000000000..a09e3715d8 --- /dev/null +++ b/apps/iatlas/api-gitlab/api/resolvers/resolver_helpers/germline_gwas_result.py @@ -0,0 +1,114 @@ +from sqlalchemy import and_ +from sqlalchemy.orm import aliased +from api import db +from api.db_models import Dataset, Feature, Snp, GermlineGwasResult +from .general_resolvers import build_join_condition, get_selected, get_value +from .data_set import build_data_set_graphql_response +from .feature import build_feature_graphql_response +from .snp import build_snp_graphql_response +from .paging_utils import get_pagination_queries + +germline_gwas_result_request_fields = {'dataSet', + 'id', + 'feature', + 'snp', + 'pValue', + 'maf'} + + +def build_ggr_graphql_response(germline_gwas_result): + return { + 'id': get_value(germline_gwas_result, 'id'), + 'pValue': get_value(germline_gwas_result, 'p_value'), + 'dataSet': build_data_set_graphql_response()(germline_gwas_result), + 'feature': build_feature_graphql_response()(germline_gwas_result), + 'snp': build_snp_graphql_response(germline_gwas_result), + 'maf': get_value(germline_gwas_result, 'maf') + } + + +def build_germline_gwas_result_request( + requested, data_set_requested, feature_requested, snp_requested, data_set=None, distinct=False, feature=None, snp=None, max_p_value=None, min_p_value=None, paging=None): + """ + Builds a SQL request. + + All positional arguments are required. Positional arguments are: + 1st position - a set of the requested fields at the root of the graphql request + 2nd position - a set of the requested fields in the 'dataset' node of the graphql request. If 'dataset' is not requested, this will be an empty set. + 3rd position - a set of the requested fields in the 'feature' node of the graphql request. If 'feature' is not requested, this will be an empty set. + 4th position - a set of the requested fields in the 'snp' node of the graphql request. If 'snp' is not requested, this will be an empty set. + + + All keyword arguments are optional. Keyword arguments are: + `dat_set` - a list of strings, data set names + `distinct` - a boolean, indicates whether duplicate records should be filtered out + `feature` - a list of strings, feature names + `snp` - a list of strings + `max_p_value` - a float, a maximum P value + `min_p_value` - a float, a minimum P value + `paging` - a dict containing pagination metadata + """ + sess = db.session + + germline_gwas_result_1 = aliased(GermlineGwasResult, name='ggr') + feature_1 = aliased(Feature, name='f') + data_set_1 = aliased(Dataset, name='ds') + snp_1 = aliased(Snp, name='snp') + + core_field_mapping = { + 'id': germline_gwas_result_1.id.label('id'), + 'pValue': germline_gwas_result_1.p_value.label('p_value'), + 'maf': germline_gwas_result_1.maf.label('maf') + } + data_set_core_field_mapping = {'display': data_set_1.display.label('data_set_display'), + 'name': data_set_1.name.label('data_set_name'), + 'type': data_set_1.dataset_type.label('data_set_type')} + feature_core_field_mapping = {'display': feature_1.display.label('feature_display'), + 'name': feature_1.name.label('feature_name'), + 'order': feature_1.order.label('feature_order'), + 'unit': feature_1.unit.label('feature_unit'), + 'germlineCategory': feature_1.germline_category.label('feature_germline_category'), + 'germlineModule': feature_1.germline_module.label('feature_germline_module')} + snp_core_field_mapping = {'rsid': snp_1.rsid.label('snp_rsid'), + 'name': snp_1.name.label('snp_name'), + 'bp': snp_1.bp.label('snp_bp'), + 'chr': snp_1.chr.label('snp_chr')} + + core = get_selected(requested, core_field_mapping) + core |= get_selected(data_set_requested, data_set_core_field_mapping) + core |= get_selected(feature_requested, feature_core_field_mapping) + core |= get_selected(snp_requested, snp_core_field_mapping) + + query = sess.query(*core) + query = query.select_from(germline_gwas_result_1) + + if max_p_value or max_p_value == 0: + query = query.filter( + germline_gwas_result_1.p_value <= float(max_p_value)) + + if min_p_value or min_p_value == 0: + query = query.filter( + germline_gwas_result_1.p_value >= float(min_p_value)) + + if 'dataSet' in requested or data_set: + is_outer = not bool(data_set) + data_set_join_condition = build_join_condition( + data_set_1.id, germline_gwas_result_1.dataset_id, filter_column=data_set_1.name, filter_list=data_set) + query = query.join(data_set_1, and_( + *data_set_join_condition), isouter=is_outer) + + if 'feature' in requested or feature: + is_outer = not bool(feature) + feature_join_condition = build_join_condition( + feature_1.id, germline_gwas_result_1.feature_id, filter_column=feature_1.name, filter_list=feature) + query = query.join(feature_1, and_( + *feature_join_condition), isouter=is_outer) + + if 'snp' in requested or snp: + is_outer = not bool(snp) + snp_join_condition = build_join_condition( + snp_1.id, germline_gwas_result_1.snp_id, filter_column=snp_1.name, filter_list=snp) + query = query.join(snp_1, and_( + *snp_join_condition), isouter=is_outer) + + return get_pagination_queries(query, paging, distinct, cursor_field=germline_gwas_result_1.id) diff --git a/apps/iatlas/api-gitlab/api/resolvers/resolver_helpers/heritability_result.py b/apps/iatlas/api-gitlab/api/resolvers/resolver_helpers/heritability_result.py new file mode 100644 index 0000000000..01e6eae4bc --- /dev/null +++ b/apps/iatlas/api-gitlab/api/resolvers/resolver_helpers/heritability_result.py @@ -0,0 +1,115 @@ +from sqlalchemy import and_ +from sqlalchemy.orm import aliased +from api import db +from api.db_models import Dataset, HeritabilityResult, Feature +from .general_resolvers import build_join_condition, get_selected, get_value +from .data_set import build_data_set_graphql_response +from .feature import build_feature_graphql_response +from .paging_utils import get_pagination_queries + +heritability_result_request_fields = {'dataSet', + 'id', + 'feature', + 'pValue', + 'cluster', + 'fdr', + 'variance', + 'se'} + + +def build_hr_graphql_response(heritability_result): + result_dict = { + 'id': get_value(heritability_result, 'id'), + 'pValue': get_value(heritability_result, 'p_value'), + 'dataSet': build_data_set_graphql_response()(heritability_result), + 'feature': build_feature_graphql_response()(heritability_result), + 'cluster': get_value(heritability_result, 'cluster'), + 'fdr': get_value(heritability_result, 'fdr'), + 'variance': get_value(heritability_result, 'variance'), + 'se': get_value(heritability_result, 'se') + } + return(result_dict) + + +def build_heritability_result_request( + requested, data_set_requested, feature_requested, data_set=None, distinct=False, feature=None, max_p_value=None, min_p_value=None, cluster=None, paging=None): + """ + Builds a SQL request. + + All positional arguments are required. Positional arguments are: + 1st position - a set of the requested fields at the root of the graphql request + 2nd position - a set of the requested fields in the 'dataSet' node of the graphql request. If 'dataSet' is not requested, this will be an empty set. + 3rd position - a set of the requested fields in the 'feature' node of the graphql request. If 'feature' is not requested, this will be an empty set. + + All keyword arguments are optional. Keyword arguments are: + `data_set` - a list of strings, data set names + `distinct` - a boolean, indicates whether duplicate records should be filtered out + `feature` - a list of strings, feature names + `max_p_value` - a float, a maximum P value + `min_p_value` - a float, a minimum P value + `cluster` a string + `paging` - a dict containing pagination metadata + """ + sess = db.session + + heritability_result_1 = aliased(HeritabilityResult, name='hr') + feature_1 = aliased(Feature, name='f') + data_set_1 = aliased(Dataset, name='ds') + + core_field_mapping = { + 'id': heritability_result_1.id.label('id'), + 'pValue': heritability_result_1.p_value.label('p_value'), + 'se': heritability_result_1.se.label('se'), + 'variance': heritability_result_1.variance.label('variance'), + 'fdr': heritability_result_1.fdr.label('fdr'), + 'cluster': heritability_result_1.cluster.label('cluster') + } + data_set_core_field_mapping = { + 'display': data_set_1.display.label('data_set_display'), + 'name': data_set_1.name.label('data_set_name'), + 'type': data_set_1.dataset_type.label('data_set_type') + } + feature_core_field_mapping = { + 'display': feature_1.display.label('feature_display'), + 'name': feature_1.name.label('feature_name'), + 'order': feature_1.order.label('feature_order'), + 'unit': feature_1.unit.label('feature_unit'), + 'germlineCategory': feature_1.germline_category.label('feature_germline_category'), + 'germlineModule': feature_1.germline_module.label('feature_germline_module') + } + + core = get_selected(requested, core_field_mapping) + core |= get_selected(data_set_requested, data_set_core_field_mapping) + core |= get_selected(feature_requested, feature_core_field_mapping) + + if distinct == False: + # Add the id as a cursor if not selecting distinct + core.add(heritability_result_1.id) + + query = sess.query(*core) + query = query.select_from(heritability_result_1) + + if cluster: + query = query.filter(heritability_result_1.cluster.in_(cluster)) + + if max_p_value or max_p_value == 0: + query = query.filter(heritability_result_1.p_value <= max_p_value) + + if min_p_value or min_p_value == 0: + query = query.filter(heritability_result_1.p_value >= min_p_value) + + if 'dataSet' in requested or data_set: + is_outer = not bool(data_set) + data_set_join_condition = build_join_condition( + data_set_1.id, heritability_result_1.dataset_id, filter_column=data_set_1.name, filter_list=data_set) + query = query.join(data_set_1, and_( + *data_set_join_condition), isouter=is_outer) + + if 'feature' in requested or feature: + is_outer = not bool(feature) + feature_join_condition = build_join_condition( + feature_1.id, heritability_result_1.feature_id, filter_column=feature_1.name, filter_list=feature) + query = query.join(feature_1, and_( + *feature_join_condition), isouter=is_outer) + + return get_pagination_queries(query, paging, distinct, cursor_field=heritability_result_1.id) diff --git a/apps/iatlas/api-gitlab/api/resolvers/resolver_helpers/mutation.py b/apps/iatlas/api-gitlab/api/resolvers/resolver_helpers/mutation.py new file mode 100644 index 0000000000..bfed01b1e1 --- /dev/null +++ b/apps/iatlas/api-gitlab/api/resolvers/resolver_helpers/mutation.py @@ -0,0 +1,225 @@ +from sqlalchemy import and_ +from sqlalchemy.orm import aliased +from itertools import groupby +from api import db +from api.db_models import Gene, Mutation, MutationType, Patient, Sample, SampleToMutation, Cohort, CohortToMutation, CohortToSample +from .general_resolvers import build_join_condition, get_selected, get_value +from .paging_utils import get_pagination_queries + +mutation_request_fields = { + 'id', + 'gene', + 'name', + 'mutationCode', + 'mutationType', + 'samples' +} + + +def get_mutation_column_labels(requested, mutation, add_id=False): + mapping = { + 'name': mutation.name.label('mutation_name'), + 'mutationCode': mutation.mutation_code.label('mutation_code') + } + labels = get_selected(requested, mapping) + + if add_id: + labels |= {mutation.id.label('mutation_id')} + + return(labels) + + +def get_mutation_type_column_labels(requested, mutation_type): + mapping = { + 'display': mutation_type.display.label('display'), + 'name': mutation_type.name.label('name') + } + labels = get_selected(requested, mapping) + return(labels) + + +def build_mutation_graphql_response(requested=[], sample_requested=[], status=None, sample=None, cohort=None, prefix='mutation_'): + from .gene import build_gene_graphql_response + from .mutation_type import build_mutation_type_graphql_response + from .sample import build_sample_graphql_response + + def f(mutation): + if not mutation: + return None + mutation_id = get_value(mutation, prefix + 'id') + samples = get_samples(mutation_id=mutation_id, requested=requested, + sample_requested=sample_requested, status=status, sample=sample, cohort=cohort) + return { + 'id': mutation_id, + 'name': get_value(mutation, prefix + 'name'), + 'mutationCode': get_value(mutation, prefix + 'code'), + 'status': get_value(mutation, prefix + 'status'), + 'gene': build_gene_graphql_response()(mutation), + 'mutationType': build_mutation_type_graphql_response(mutation), + 'samples': map(build_sample_graphql_response(), samples) + } + return f + + +def build_mutation_request(requested, gene_requested, mutation_type_requested, distinct=False, paging=None, cohort=None, entrez=None, mutation=None, mutation_code=None, mutation_type=None, sample=None): + ''' + Builds a SQL request + + All positional arguments are required. Positional arguments are: + 1st position - a set of the requested fields at the root of the graphql request + 2nd position - a set of the requested fields in the 'gene' node of the graphql request. If 'gene' is not requested, this will be an empty set. + 3rd position - a set of the requested fields in the 'mutationType' node of the graphql request. If 'mutationType' is not requested, this will be an empty set. + + All keyword arguments are optional. Keyword arguments are: + `distinct` - a boolean, indicates whether duplicate records should be filtered out + `paging` - a dict containing pagination metadata + `cohort` - a list of strings, cohort names + `entrez` - a list of integers, gene entrez ids + `mutation` - a list of strings, mutation names + `mutation_code` - a list of strings, mutation codes + `mutation_type` - a list of strings, mutation type names + `sample` - a list of strings, sample names + ''' + from .gene import get_simple_gene_column_labels + sess = db.session + + gene_1 = aliased(Gene, name='g') + mutation_1 = aliased(Mutation, name='m') + mutation_type_1 = aliased(MutationType, name='mt') + sample_1 = aliased(Sample, name='s') + sample_to_mutation_1 = aliased(SampleToMutation, name='sm') + cohort_1 = aliased(Cohort, name='c') + cohort_to_mutation_1 = aliased(CohortToMutation, name='ctm') + + mutation_core = get_mutation_column_labels( + requested, mutation_1, add_id=True) + + gene_core = get_simple_gene_column_labels(gene_requested, gene_1) + + mutation_type_core = get_mutation_type_column_labels( + mutation_type_requested, mutation_type_1) + + query = sess.query(*[*mutation_core, *gene_core, *mutation_type_core]) + query = query.select_from(mutation_1) + + if mutation: + query = query.filter(mutation_1.name.in_(mutation)) + + query = build_simple_mutation_request( + query, requested, mutation_1, gene_1, mutation_type_1, + entrez=entrez, + mutation_code=mutation_code, + mutation_type=mutation_type + ) + + if sample: + sample_subquery = sess.query( + sample_to_mutation_1.mutation_id) + + sample_join_condition = build_join_condition( + sample_to_mutation_1.sample_id, sample_1.id, filter_column=sample_1.name, filter_list=sample) + + sample_subquery = sample_subquery.join(sample_1, and_( + *sample_join_condition), isouter=False) + + query = query.filter(mutation_1.id.in_(sample_subquery)) + + if cohort: + cohort_subquery = sess.query(cohort_to_mutation_1.mutation_id) + + cohort_join_condition = build_join_condition( + cohort_to_mutation_1.cohort_id, cohort_1.id, filter_column=cohort_1.name, filter_list=cohort) + + cohort_subquery = cohort_subquery.join(cohort_1, and_( + *cohort_join_condition), isouter=False) + + query = query.filter(mutation_1.id.in_(cohort_subquery)) + + return get_pagination_queries(query, paging, distinct, cursor_field=mutation_1.id) + + +def build_simple_mutation_request(query, requested, mutation_obj, gene_obj, mutation_type_obj, entrez=None, mutation_code=None, mutation_type=None): + ''' + Adds to a SQL query + + All positional arguments are required. Positional arguments are: + 1st position - a sql query + 2nd position - a set of the requested fields at the root of the graphql request + 3rd position - a mutation table object + 4th position - a gene table object + 5th position - a mutation type table object + All keyword arguments are optional. Keyword arguments are: + `entrez` - a list of integers, gene entrez ids + `mutation` - a list of strings, mutation names + `mutation_code` - a list of strings, mutation codes + `mutation_type` - a list of strings, mutation type names + ''' + + if 'gene' in requested or entrez: + is_outer = not bool(entrez) + gene_join_condition = build_join_condition( + gene_obj.id, mutation_obj.gene_id, + filter_column=gene_obj.entrez_id, + filter_list=entrez + ) + query = query.join(gene_obj, and_( + *gene_join_condition), isouter=is_outer) + + if 'mutationType' in requested or mutation_type: + is_outer = not bool(mutation_type) + mutation_type_join_condition = build_join_condition( + mutation_type_obj.id, mutation_obj.mutation_type_id, filter_column=mutation_type_obj.name, filter_list=mutation_type) + query = query.join(mutation_type_obj, and_( + *mutation_type_join_condition), isouter=is_outer) + + if mutation_code: + query = query.filter(mutation_obj.mutation_code.in_(mutation_code)) + + return(query) + + +def get_samples(mutation_id, requested, sample_requested, status=None, sample=None, cohort=None): + + if 'samples' not in requested: + return [] + + sess = db.session + + sample_1 = aliased(Sample, name='s') + sample_to_mutation_1 = aliased(SampleToMutation, name='stm') + cohort_1 = aliased(Cohort, name='c') + cohort_to_sample_1 = aliased(CohortToSample, name='cts') + + core_field_mapping = { + 'name': sample_1.name.label('sample_name'), + 'status': sample_to_mutation_1.mutation_status.label('sample_mutation_status') + } + + core = get_selected(sample_requested, core_field_mapping) + + query = sess.query(*core) + query = query.select_from(sample_to_mutation_1) + query = query.filter(sample_to_mutation_1.mutation_id == mutation_id) + + if status: + query = query.filter(sample_to_mutation_1.mutation_status.in_(status)) + + sample_join_condition = build_join_condition( + sample_to_mutation_1.sample_id, sample_1.id, filter_column=sample_1.name, filter_list=sample) + + query = query.join( + sample_1, and_(*sample_join_condition)) + + if cohort: + cohort_subquery = sess.query(cohort_to_sample_1.sample_id) + + cohort_join_condition = build_join_condition( + cohort_to_sample_1.cohort_id, cohort_1.id, filter_column=cohort_1.name, filter_list=cohort) + + cohort_subquery = cohort_subquery.join(cohort_1, and_( + *cohort_join_condition), isouter=False) + + query = query.filter( + sample_to_mutation_1.sample_id.in_(cohort_subquery)) + + return query.all() diff --git a/apps/iatlas/api-gitlab/api/resolvers/resolver_helpers/mutation_type.py b/apps/iatlas/api-gitlab/api/resolvers/resolver_helpers/mutation_type.py new file mode 100644 index 0000000000..e71bdb6960 --- /dev/null +++ b/apps/iatlas/api-gitlab/api/resolvers/resolver_helpers/mutation_type.py @@ -0,0 +1,47 @@ +from sqlalchemy import and_, orm +from api import db +from api.db_models import MutationType +from .general_resolvers import get_selected, get_value + +mutation_type_request_fields = {'display', 'name'} + + +def build_mutation_type_graphql_response(mutation_type): + if not mutation_type: + return None + return { + 'display': get_value(mutation_type, 'display'), + 'name': get_value(mutation_type) + } + + +def build_mutation_type_request(requested): + """ + Builds a SQL request. + """ + sess = db.session + + mutation_type_1 = orm.aliased(MutationType, name='mt') + + core_field_mapping = {'display': mutation_type_1.display.label('display'), + 'name': mutation_type_1.name.label('name')} + core = get_selected(requested, core_field_mapping) + + query = sess.query(*core) + query = query.select_from(mutation_type_1) + + order = [] + append_to_order = order.append + if 'name' in requested: + append_to_order(mutation_type_1.name) + if 'display' in requested: + append_to_order(mutation_type_1.display) + + query = query.order_by(*order) if order else query + + return query + + +def request_mutation_types(requested): + query = build_mutation_type_request(requested) + return query.all() diff --git a/apps/iatlas/api-gitlab/api/resolvers/resolver_helpers/neoantigen.py b/apps/iatlas/api-gitlab/api/resolvers/resolver_helpers/neoantigen.py new file mode 100644 index 0000000000..f932a596e9 --- /dev/null +++ b/apps/iatlas/api-gitlab/api/resolvers/resolver_helpers/neoantigen.py @@ -0,0 +1,105 @@ +from sqlalchemy import and_ +from sqlalchemy.orm import aliased +from api import db +from api.db_models import Neoantigen, Gene, Patient +from .general_resolvers import build_join_condition, get_selected, get_value +from .gene import build_gene_graphql_response +from .patient import build_patient_graphql_response +from .paging_utils import get_pagination_queries + + +neoantigen_request_fields = { + "id", + "pmhc", + "freqPmhc", + "tpm", + "gene", + "patient", +} + + +def build_neoantigen_graphql_response(neoantigen): + result_dict = { + 'id': get_value(neoantigen, 'id'), + 'pmhc': get_value(neoantigen, 'pmhc'), + 'freqPmhc': get_value(neoantigen, 'freq_pmhc'), + 'tpm': get_value(neoantigen, 'tpm'), + 'gene': build_gene_graphql_response()(neoantigen), + 'patient': build_patient_graphql_response()(neoantigen), + } + return(result_dict) + + +def build_neoantigen_request( + requested, + patient_requested, + gene_requested, + distinct=False, + paging=None, + patient=None, + entrez=None, + pmhc=None, +): + sess = db.session + + neoantigen_1 = aliased(Neoantigen, name='n') + gene_1 = aliased(Gene, name='g') + patient_1 = aliased(Patient, name='p') + + core_field_mapping = { + 'id': neoantigen_1.id.label('id'), + 'tpm': neoantigen_1.tpm.label('tpm'), + 'pmhc': neoantigen_1.pmhc.label('pmhc'), + 'freqPmhc': neoantigen_1.freq_pmhc.label('freq_pmhc'), + } + gene_core_field_mapping = { + 'entrez': gene_1.entrez_id.label('gene_entrez'), + 'hgnc': gene_1.hgnc_id.label('gene_hgnc'), + } + patient_core_field_mapping = { + 'barcode': patient_1.name.label('patient_barcode'), + } + + core = get_selected(requested, core_field_mapping) + core |= get_selected(gene_requested, gene_core_field_mapping) + core |= get_selected(patient_requested, patient_core_field_mapping) + + if distinct == False: + # Add the id as a cursor if not selecting distinct + core.add(neoantigen_1.id) + + query = sess.query(*core) + query = query.select_from(neoantigen_1) + + if pmhc: + query = query.filter(neoantigen_1.pmhc.in_(pmhc)) + + if 'gene' in requested or entrez: + is_outer = not bool(entrez) + gene_join_condition = build_join_condition( + gene_1.id, + neoantigen_1.neoantigen_gene_id, + filter_column=gene_1.entrez_id, + filter_list=entrez + ) + query = query.join( + gene_1, + and_(*gene_join_condition), + isouter=is_outer + ) + + if 'patient' in requested or patient: + is_outer = not bool(patient) + patient_join_condition = build_join_condition( + patient_1.id, + neoantigen_1.patient_id, + filter_column=patient_1.name, + filter_list=patient + ) + query = query.join( + patient_1, + and_(*patient_join_condition), + isouter=is_outer + ) + + return get_pagination_queries(query, paging, distinct, cursor_field=neoantigen_1.id) diff --git a/apps/iatlas/api-gitlab/api/resolvers/resolver_helpers/node.py b/apps/iatlas/api-gitlab/api/resolvers/resolver_helpers/node.py new file mode 100644 index 0000000000..d2200295ce --- /dev/null +++ b/apps/iatlas/api-gitlab/api/resolvers/resolver_helpers/node.py @@ -0,0 +1,260 @@ +from itertools import groupby +from sqlalchemy import and_, func +from sqlalchemy.orm import aliased +from api import db +from api.db_models import Dataset, DatasetToTag, Feature, Gene, GeneToGeneSet, GeneSet, Node, Tag +from .general_resolvers import build_join_condition, get_selected, get_value +from .paging_utils import get_pagination_queries + +simple_node_request_fields = { + 'label', + 'name', + 'network', + 'score', + 'x', + 'y', +} + +node_request_fields = simple_node_request_fields.union({ + 'dataSet', + 'feature', + 'gene', + 'tags', +}) + + +def get_node_column_labels(requested, node, prefix='node_', add_id=False): + mapping = { + 'label': node.label.label(prefix + 'label'), + 'name': node.name.label(prefix + 'name'), + 'network': node.network.label(prefix + 'network'), + 'score': node.score.label(prefix + 'score'), + 'x': node.x.label(prefix + 'x'), + 'y': node.y.label(prefix + 'y') + } + labels = get_selected(requested, mapping) + + if add_id: + labels |= {node.id.label('id')} + + return(labels) + + +def build_node_graphql_response(requested=[], prefix='node_'): + from .data_set import build_data_set_graphql_response + from .feature import build_feature_graphql_response + from .gene import build_gene_graphql_response + from .tag import build_tag_graphql_response + + def f(node): + if not node: + return None + else: + node_id = get_value(node, 'id') + has_tag1 = get_value(node, 'tag_1_name') + has_tag2 = get_value(node, 'tag_2_name') + has_feature = get_value(node, 'feature_name') or get_value( + node, 'feature_display') or get_value(node, 'feature_order') or get_value(node, 'feature_unit') + has_gene = get_value(node, 'gene_entrez') or get_value(node, 'gene_hgnc') or get_value( + node, 'gene_description') or get_value(node, 'gene_friendly_name') or get_value(node, 'gene_io_landscape_name') + dict = { + 'id': node_id, + 'label': get_value(node, prefix + 'label'), + 'name': get_value(node, prefix + 'name'), + 'network': get_value(node, prefix + 'network'), + 'score': get_value(node, prefix + 'score'), + 'x': get_value(node, prefix + 'x'), + 'y': get_value(node, prefix + 'y'), + 'dataSet': build_data_set_graphql_response()(node), + 'feature': build_feature_graphql_response()(node) if has_feature else None, + 'gene': build_gene_graphql_response()(node) if has_gene else None, + 'tag1': build_tag_graphql_response(prefix='tag_1_')(node) if has_tag1 else None, + 'tag2': build_tag_graphql_response(prefix='tag_2_')(node) if has_tag2 else None, + } + return(dict) + return(f) + + +def build_node_request( + requested, + data_set_requested, + feature_requested, + gene_requested, + tag_requested1, + tag_requested2, + data_set=None, + distinct=False, + entrez=None, + feature=None, + feature_class=None, + gene_type=None, + max_score=None, + min_score=None, + network=None, + paging=None, + related=None, + tag1=None, + tag2=None, + n_tags=None + ): + ''' + Builds a SQL request. + + All positional arguments are required. Positional arguments are: + 1st position - a set of the requested fields at the root of the graphql request + 2nd position - a set of the requested fields in the 'dataSet' node of the graphql request. If 'dataSet' is not requested, this will be an empty set. + 3rd position - a set of the requested fields in the 'feature' node of the graphql request. If 'feature' is not requested, this will be an empty set. + 4th position - a set of the requested fields in the 'gene' node of the graphql request. If 'gene' is not requested, this will be an empty set. + + All keyword arguments are optional. Keyword arguments are: + `data_set` - a list of strings, data set names + `distinct` - a boolean, specifies whether or not duplicates should be filtered out + `entrez` - a list of integers, gene entrez ids + `feature` - a list of strings, feature names + `feature_class` - a list of strings, feature class names + `gene_type` - a list of strings, gene type names + `max_score` - a float, a maximum score value + `min_score` - a float, a minimum score value + `network` - a list of strings + 'paging' - an instance of PagingInput + `type` - a string, the type of pagination to perform. Must be either 'OFFSET' or 'CURSOR'." + `page` - an integer, when performing OFFSET paging, the page number requested. + `limit` - an integer, when performing OFFSET paging, the number or records requested. + `first` - an integer, when performing CURSOR paging, the number of records requested AFTER the CURSOR. + `last` - an integer, when performing CURSOR paging, the number of records requested BEFORE the CURSOR. + `before` - an integer, when performing CURSOR paging: the CURSOR to be used in tandem with 'last' + `after` - an integer, when performing CURSOR paging: the CURSOR to be used in tandem with 'first' + `related` - a list of strings, tag names related to data sets + `tag1` - a list of strings, tag names + `tag2` - a list of strings, tag names + `n_tags` - the number of tags the node should have + ''' + from .tag import get_tag_column_labels + + sess = db.session + + data_set_1 = aliased(Dataset, name='d') + feature_1 = aliased(Feature, name='f') + gene_1 = aliased(Gene, name='g') + node_1 = aliased(Node, name='n') + tag_1 = aliased(Tag, name="t1") + tag_2 = aliased(Tag, name="t2") + + + data_set_field_mapping = { + 'display': data_set_1.display.label('data_set_display'), + 'name': data_set_1.name.label('data_set_name'), + 'type': data_set_1.dataset_type.label('data_set_type') + } + + feature_field_mapping = { + 'display': feature_1.display.label('feature_display'), + 'name': feature_1.name.label('feature_name'), + 'order': feature_1.order.label('feature_order'), + 'unit': feature_1.unit.label('feature_unit') + } + + gene_field_mapping = { + 'entrez': gene_1.entrez_id.label('gene_entrez'), + 'hgnc': gene_1.hgnc_id.label('gene_hgnc'), + 'description': gene_1.description.label('gene_description'), + 'friendlyName': gene_1.friendly_name.label('gene_friendly_name'), + 'ioLandscapeName': gene_1.io_landscape_name.label('gene_io_landscape_name') + } + + node_core = get_node_column_labels(requested, node_1, add_id=True) + data_set_core = get_selected(data_set_requested, data_set_field_mapping) + feature_core = get_selected(feature_requested, feature_field_mapping) + gene_core = get_selected(gene_requested, gene_field_mapping) + tag_core1 = get_tag_column_labels(tag_requested1, tag_1, prefix='tag_1_') + tag_core2 = get_tag_column_labels(tag_requested2, tag_2, prefix='tag_2_') + + query = sess.query( + *[*node_core, *data_set_core, *feature_core, *gene_core, *tag_core1, *tag_core2]) + query = query.select_from(node_1) + + if max_score: + query = query.filter(node_1.score <= max_score) + + if min_score: + query = query.filter(node_1.score >= min_score) + + if network: + query = query.filter(node_1.network.in_(network)) + + if tag1 or tag_requested1: + is_outer = not bool(tag1) + tag_join_condition = build_join_condition( + tag_1.id, + node_1.tag_1_id, + filter_column=tag_1.name, + filter_list=tag1 + ) + query = query.join(tag_1, and_( + *tag_join_condition), isouter=is_outer) + + if n_tags or tag2 or tag_requested1: + if tag2: + tag_join_condition = build_join_condition( + tag_2.id, + node_1.tag_2_id, + filter_column=tag_2.name, + filter_list=tag2 + ) + query = query.join( + tag_2, + and_( *tag_join_condition), + isouter=False + ) + else: + tag_join_condition = build_join_condition( + tag_2.id, + node_1.tag_2_id, + ) + query = query.join(tag_2, and_(*tag_join_condition), isouter=True) + if n_tags == 1: + query = query.filter(tag_2.id.is_(None)) + if n_tags == 2: + query = query.filter(tag_2.id.isnot(None)) + + if data_set or related or 'dataSet' in requested: + data_set_join_condition = build_join_condition( + data_set_1.id, node_1.dataset_id, data_set_1.name, data_set) + query = query.join(data_set_1, and_(*data_set_join_condition)) + + if related: + data_set_to_tag_1 = aliased(DatasetToTag, name='dtt') + related_tag_1 = aliased(Tag, name='rt') + + related_tag_sub_query = sess.query(related_tag_1.id).filter( + related_tag_1.name.in_(related)) + + data_set_tag_join_condition = build_join_condition( + data_set_to_tag_1.dataset_id, data_set_1.id, data_set_to_tag_1.tag_id, related_tag_sub_query) + query = query.join( + data_set_to_tag_1, and_(*data_set_tag_join_condition)) + + if feature or 'feature' in requested or feature_class: + is_outer = not bool(feature) + feature_join_condition = build_join_condition( + feature_1.id, node_1.node_feature_id, feature_1.name, feature) + query = query.join(feature_1, and_( + *feature_join_condition), isouter=is_outer) + + if feature_class: + query = query.filter(feature_1.feature_class.in_(feature_class)) + + if entrez or 'gene' in requested or gene_type: + is_outer = not bool(entrez) + gene_join_condition = build_join_condition( + gene_1.id, node_1.node_gene_id, gene_1.entrez_id, entrez) + query = query.join(gene_1, and_( + *gene_join_condition), isouter=is_outer) + + if gene_type: + gene_type_1 = aliased(GeneSet, name='gt') + gene_to_type_1 = aliased(GeneToGeneSet, name='ggt') + query = query.join(gene_to_type_1, and_( + gene_to_type_1.gene_id == gene_1.id, gene_to_type_1.gene_set_id.in_(sess.query(gene_type_1.id).filter(gene_type_1.name.in_(gene_type))))) + + return get_pagination_queries(query, paging, distinct, cursor_field=node_1.id) diff --git a/apps/iatlas/api-gitlab/api/resolvers/resolver_helpers/paging_utils.py b/apps/iatlas/api-gitlab/api/resolvers/resolver_helpers/paging_utils.py new file mode 100644 index 0000000000..7dcbe3a376 --- /dev/null +++ b/apps/iatlas/api-gitlab/api/resolvers/resolver_helpers/paging_utils.py @@ -0,0 +1,180 @@ +import base64 +import math +import uuid +from collections import deque +from api.database.database_helpers import temp_table, execute_sql + + +class Paging: + OFFSET = 'OFFSET' + CURSOR = 'CURSOR' + MAX_LIMIT = 100000 + ASC = 'ASC' + DESC = 'DESC' + DEFAULT = {'type': CURSOR, 'first': MAX_LIMIT} + + +paging_fields = {'type', 'page', 'pages', + 'total', 'first', 'last', 'before', 'after'} + + +def to_cursor_hash(val): + return str(base64.b64encode(str(val).encode("utf-8")), "utf-8") + + +def from_cursor_hash(encoded): + return str(base64.b64decode(str(encoded)), "utf-8") + + +def get_cursor(before, after): + if after != None: + return (from_cursor_hash(after), Paging.ASC) + if before != None: + return (from_cursor_hash(before), Paging.DESC) + return (None, Paging.ASC) + + +def parse_limit(n, max=Paging.MAX_LIMIT): + return min(max, int(n)) + + +def get_limit(first, last, limit, max=Paging.MAX_LIMIT): + if first and not math.isnan(first): + return (parse_limit(first, max), Paging.ASC) + if last and not math.isnan(last): + return (parse_limit(last, max), Paging.DESC) + if limit and not math.isnan(limit): + return (parse_limit(limit, max), Paging.ASC) + return (max, Paging.ASC) + + +def get_pagination_queries(query, paging, distinct, cursor_field=None): + count_query = query + if paging.get('type', Paging.CURSOR) == Paging.OFFSET or distinct == True: + if distinct == True: + return query.distinct(), count_query.distinct() + return query, count_query + # Handle cursor and sort order + cursor, sort_order = get_cursor(paging.get('before'), paging.get('after')) + order_by = cursor_field + if sort_order == Paging.ASC: + query = query.order_by(order_by) + else: + query = query.order_by(order_by.desc()) + if cursor: + if sort_order == Paging.ASC: + query = query.filter(cursor_field > cursor) + else: + query = query.filter(cursor_field < cursor) + # end handle cursor + return query, count_query + + +def create_temp_table(query, paging, distinct): + paging_type = paging.get('type', Paging.CURSOR) + page = None + before = None + after = None + first = paging.get('first') + last = paging.get('last') + limit = paging.get('limit') + limit, sort_order = get_limit(first, last, limit) + table_name = f'_temp_{uuid.uuid4()}'.replace('-', '') + if paging_type == Paging.OFFSET or distinct == True: + page = paging.get('page', 1) + # run the offset query + query = query.limit(limit) + query = query.offset((page - 1) * limit) + else: + # request 1 more than we need, so we can determine if additional pages are available. returns list. + # run the cursor query + # Store query results in temp table + query = query.limit(limit + 1) + conn = temp_table(table_name, query) + # items = query.all() # slower than querying the new temp table because we have to recreate filters and joins + # instead grab everything from the new temp table + item_query = f'SELECT * FROM {table_name}' + items = execute_sql(item_query, conn=conn) + return items, table_name, conn + + +def fetch_page(query, paging, distinct): + max = paging.get('max', Paging.MAX_LIMIT) + paging_type = paging.get('type', Paging.CURSOR) + page = paging.get('page', 1) + first = paging.get('first') + last = paging.get('last') + limit = paging.get('limit') + limit, order = get_limit(first, last, limit, max) + if paging_type == Paging.OFFSET or distinct == True: + if distinct: + query = query.distinct() + return query.paginate(page, limit).items + res = query.limit(limit + 1).all() + return res + + +def process_page(items, count_query, paging, distinct, response_builder, pagination_requested): + paging = paging if paging else {} + paging_type = paging.get('type', Paging.CURSOR) + page = None + max = paging.get('max', Paging.MAX_LIMIT) + first = paging.get('first') + last = paging.get('last') + limit = paging.get('limit') + limit, order = get_limit(first, last, limit, max) + pageInfo = { + 'type': paging_type, + 'page': page, + 'pages': None, + 'limit': limit, + 'returned': None, + 'total': None + } + if paging_type == Paging.OFFSET or distinct == True: + # if distinct is True, paging type must be OFFSET + pageInfo['type'] = Paging.OFFSET + pageInfo['page'] = paging.get('page', 1) + results = map(response_builder, items) if response_builder else items + else: + returned = len(items) + if order == Paging.ASC: + hasNextPage = items != None and returned == limit + 1 + pageInfo['hasNextPage'] = hasNextPage + pageInfo['hasPreviousPage'] = False + if hasNextPage: + items.pop(-1) # remove the extra last item + if order == Paging.DESC: + items.reverse() # We have to reverse the list to get previous pages in the expected order + pageInfo['hasNextPage'] = False + hasPreviousPage = items != None and returned == limit + 1 + pageInfo['hasPreviousPage'] = hasPreviousPage + if hasPreviousPage: + items.pop(0) # remove the extra first item + results = deque(map(response_builder, items) + if response_builder else items) + pageInfo['startCursor'] = to_cursor_hash( + results[0]['id']) if (len(results) > 0) else None + pageInfo['endCursor'] = to_cursor_hash( + results[-1]['id']) if (len(results) > 0) else None + if 'total' in pagination_requested or 'pages' in pagination_requested: + # TODO: Consider caching this value per query, and/or making count query in parallel + count = count_query.count() + pageInfo['total'] = count + pageInfo['pages'] = math.ceil(count / limit) + pageInfo['returned'] = len(items) + return { + 'items': results, + 'paging': pageInfo + } + + +def paginate(query, count_query, paging, distinct, response_builder, pagination_requested): + items = fetch_page(query, paging, distinct) + return process_page(items, count_query, paging, distinct, response_builder, pagination_requested) + + +def create_paging(paging=None, max_results=Paging.MAX_LIMIT): + paging = paging if paging else Paging.DEFAULT.copy() + paging['max'] = max_results + return(paging) diff --git a/apps/iatlas/api-gitlab/api/resolvers/resolver_helpers/patient.py b/apps/iatlas/api-gitlab/api/resolvers/resolver_helpers/patient.py new file mode 100644 index 0000000000..028e37c444 --- /dev/null +++ b/apps/iatlas/api-gitlab/api/resolvers/resolver_helpers/patient.py @@ -0,0 +1,215 @@ +from sqlalchemy import and_ +from sqlalchemy.orm import aliased +from itertools import groupby +from api import db +from api.db_models import Dataset, DatasetToSample, Patient, Sample, Slide +from .general_resolvers import build_join_condition, get_selected, get_value +from .paging_utils import get_pagination_queries + +simple_patient_request_fields = { + 'ageAtDiagnosis', + 'barcode', + 'ethnicity', + 'gender', + 'height', + 'race', + 'weight' +} + +patient_request_fields = simple_patient_request_fields.union( + {'samples', 'slides'}) + + +def build_patient_graphql_response(requested=dict(), slide_requested=dict(), sample=None, slide=None, prefix='patient_'): + from .slide import build_slide_graphql_response + + def f(patient): + if not patient: + return None + else: + patient_id = get_value(patient, prefix + 'id') + samples = get_samples(patient_id, requested, sample) + slides = get_slides(patient_id, requested, slide_requested, slide) + dict = { + 'id': patient_id, + 'ageAtDiagnosis': get_value(patient, prefix + 'age_at_diagnosis'), + 'barcode': get_value(patient, prefix + 'barcode'), + 'ethnicity': get_value(patient, prefix + 'ethnicity'), + 'gender': get_value(patient, prefix + 'gender'), + 'height': get_value(patient, prefix + 'height'), + 'race': get_value(patient, prefix + 'race'), + 'weight': get_value(patient, prefix + 'weight'), + 'samples': map(get_value, samples), + 'slides': map(build_slide_graphql_response(), slides) + } + return(dict) + return f + + +def build_patient_request( + requested, distinct=False, paging=None, max_age_at_diagnosis=None, min_age_at_diagnosis=None, barcode=None, data_set=None, ethnicity=None, gender=None, max_height=None, min_height=None, + race=None, max_weight=None, min_weight=None, sample=None, slide=None +): + """ + Builds a SQL query. + """ + sess = db.session + + patient_1 = aliased(Patient, name='p') + sample_1 = aliased(Sample, name='s') + slide_1 = aliased(Slide, name='sd') + + core_field_mapping = { + 'ageAtDiagnosis': patient_1.age_at_diagnosis.label('patient_age_at_diagnosis'), + 'barcode': patient_1.name.label('patient_barcode'), + 'ethnicity': patient_1.ethnicity.label('patient_ethnicity'), + 'gender': patient_1.gender.label('patient_gender'), + 'height': patient_1.height.label('patient_height'), + 'race': patient_1.race.label('patient_race'), + 'weight': patient_1.weight.label('patient_weight') + } + + core = get_selected(requested, core_field_mapping) + core.add(patient_1.id.label('patient_id')) + + query = sess.query(*core) + query = query.select_from(patient_1) + + if barcode: + query = query.filter(patient_1.name.in_(barcode)) + + if max_age_at_diagnosis: + query = query.filter(patient_1.age_at_diagnosis <= + max_age_at_diagnosis) + + if min_age_at_diagnosis: + query = query.filter(patient_1.age_at_diagnosis >= + min_age_at_diagnosis) + + if ethnicity: + query = query.filter(patient_1.ethnicity.in_(ethnicity)) + + if gender: + query = query.filter(patient_1.gender.in_(gender)) + + if max_height: + query = query.filter(patient_1.height <= max_height) + + if min_height: + query = query.filter(patient_1.height >= min_height) + + if race: + query = query.filter(patient_1.race.in_(race)) + + if max_weight: + query = query.filter(patient_1.weight <= max_weight) + + if min_weight: + query = query.filter(patient_1.weight >= min_weight) + + if sample or data_set: + data_set_1 = aliased(Dataset, name='d') + data_set_to_sample_1 = aliased(DatasetToSample, name='ds') + + is_outer = not bool(sample) + + sample_join_condition = build_join_condition( + patient_1.id, sample_1.patient_id, filter_column=sample_1.name, filter_list=sample) + query = query.join(sample_1, and_( + *sample_join_condition), isouter=is_outer) + + data_set_sub_query = sess.query(data_set_1.id).filter( + data_set_1.name.in_(data_set)) if data_set else None + + data_set_to_sample_join_condition = build_join_condition( + data_set_to_sample_1.sample_id, sample_1.id, data_set_to_sample_1.dataset_id, data_set_sub_query) + query = query.join(data_set_to_sample_1, and_( + *data_set_to_sample_join_condition)) + + if slide: + slide_join_condition = build_join_condition( + patient_1.id, slide_1.patient_id, filter_column=slide_1.name, filter_list=slide) + query = query.join(slide_1, and_( + *slide_join_condition), isouter=False) + + order = [] + append_to_order = order.append + if 'barcode' in requested: + append_to_order(patient_1.name) + if 'ageAtDiagnosis' in requested: + append_to_order(patient_1.age_at_diagnosis) + if 'gender' in requested: + append_to_order(patient_1.gender) + if 'race' in requested: + append_to_order(patient_1.race) + if 'ethnicity' in requested: + append_to_order(patient_1.ethnicity) + if 'weight' in requested: + append_to_order(patient_1.weight) + if 'height' in requested: + append_to_order(patient_1.height) + + query = query.order_by(*order) if order else query + + return get_pagination_queries(query, paging, distinct, cursor_field=patient_1.id) + + +def get_samples(id, requested, sample=None): + + if 'samples' in requested: + + sess = db.session + sample_1 = aliased(Sample, name='s') + core = {sample_1.name.label('name')} + query = sess.query(*core) + query = query.select_from(sample_1) + + query = query.filter(sample_1.patient_id == id) + + if sample: + query = query.filter(sample_1.name.in_(sample)) + + order = [] + append_to_order = order.append + if 'name' in requested: + append_to_order(sample_1.name) + + query = query.order_by(*order) if order else query + return query.all() + + return [] + + +def get_slides(id, requested, slide_requested, slide=None): + if 'slides' not in requested: + return [] + + else: + sess = db.session + slide_1 = aliased(Slide, name='sd') + + core_field_mapping = { + 'description': slide_1.description.label('slide_description'), + 'name': slide_1.name.label('slide_name') + } + + core = get_selected(slide_requested, core_field_mapping) + + query = sess.query(*core) + query = query.select_from(slide_1) + + query = query.filter(slide_1.patient_id == id) + + if slide: + query = query.filter(slide_1.name.in_(slide)) + + order = [] + append_to_order = order.append + if 'name' in slide_requested: + append_to_order(slide_1.name) + if 'description' in slide_requested: + append_to_order(slide_1.description) + + query = query.order_by(*order) if order else query + + return query.all() diff --git a/apps/iatlas/api-gitlab/api/resolvers/resolver_helpers/publication.py b/apps/iatlas/api-gitlab/api/resolvers/resolver_helpers/publication.py new file mode 100644 index 0000000000..7ad7371340 --- /dev/null +++ b/apps/iatlas/api-gitlab/api/resolvers/resolver_helpers/publication.py @@ -0,0 +1,21 @@ +from .general_resolvers import get_value + +simple_publication_request_fields = { + 'doId', 'firstAuthorLastName', 'journal', 'name', 'pubmedId', 'title', 'year'} + +publication_request_fields = simple_publication_request_fields.union({ + 'genes', 'geneTypes'}) + + +def build_publication_graphql_response(pub): + if not pub: + return None + return { + 'firstAuthorLastName': get_value(pub, 'first_author_last_name'), + 'doId': get_value(pub, 'do_id'), + 'journal': get_value(pub, 'journal'), + 'name': get_value(pub, 'publication_name') or get_value(pub), + 'pubmedId': get_value(pub, 'pubmed_id'), + 'title': get_value(pub, 'title'), + 'year': get_value(pub, 'year') + } diff --git a/apps/iatlas/api-gitlab/api/resolvers/resolver_helpers/rare_variant_pathway_association.py b/apps/iatlas/api-gitlab/api/resolvers/resolver_helpers/rare_variant_pathway_association.py new file mode 100644 index 0000000000..be0daefcba --- /dev/null +++ b/apps/iatlas/api-gitlab/api/resolvers/resolver_helpers/rare_variant_pathway_association.py @@ -0,0 +1,131 @@ +from sqlalchemy import and_ +from sqlalchemy.orm import aliased +from api import db +from api.db_models import Dataset, RareVariantPathwayAssociation, Feature +from .general_resolvers import build_join_condition, get_selected, get_value +from .data_set import build_data_set_graphql_response +from .feature import build_feature_graphql_response +from .paging_utils import get_pagination_queries + +rare_variant_pathway_association_request_fields = { + 'id', + 'dataSet', + 'feature', + 'pathway', + 'pValue', + 'min', + 'max', + 'mean', + 'q1', + 'q2', + 'q3', + 'nTotal', + 'nMutants' +} + + +def build_rvpa_graphql_response(rare_variant_pathway_association): + return { + 'id': get_value(rare_variant_pathway_association, 'id'), + 'dataSet': build_data_set_graphql_response()(rare_variant_pathway_association), + 'feature': build_feature_graphql_response()(rare_variant_pathway_association), + 'pathway': get_value(rare_variant_pathway_association, 'pathway'), + 'pValue': get_value(rare_variant_pathway_association, 'p_value'), + 'min': get_value(rare_variant_pathway_association, 'min'), + 'max': get_value(rare_variant_pathway_association, 'max'), + 'mean': get_value(rare_variant_pathway_association, 'mean'), + 'q1': get_value(rare_variant_pathway_association, 'q1'), + 'q2': get_value(rare_variant_pathway_association, 'q2'), + 'q3': get_value(rare_variant_pathway_association, 'q3'), + 'nMutants': get_value(rare_variant_pathway_association, 'n_mutants'), + 'nTotal': get_value(rare_variant_pathway_association, 'n_total'), + + } + + +def build_rare_variant_pathway_association_request( + requested, data_set_requested, feature_requested, distinct=False, paging=None, data_set=None, feature=None, pathway=None, max_p_value=None, min_p_value=None): + """ + Builds a SQL request. + + All positional arguments are required. Positional arguments are: + 1st position - a set of the requested fields at the root of the graphql request + 2nd position - a set of the requested fields in the 'dataSet' node of the graphql request. If 'dataSet' is not requested, this will be an empty set. + 3rd position - a set of the requested fields in the 'feature' node of the graphql request. If 'feature' is not requested, this will be an empty set. + + All keyword arguments are optional. Keyword arguments are: + `distinct` - a boolean, indicates whether duplicate records should be filtered out + `paging` - a dict containing pagination metadata + `data_set` - a list of strings, data set names + `pathway` - a list of strings, pathway names + `feature` - a list of strings, feature names + `max_p_value` - a float, a maximum P value + `min_p_value` - a float, a minimum P value + """ + sess = db.session + + rare_variant_pathway_association_1 = aliased( + RareVariantPathwayAssociation, name='rvpa') + feature_1 = aliased(Feature, name='f') + data_set_1 = aliased(Dataset, name='ds') + + core_field_mapping = { + 'id': rare_variant_pathway_association_1.id.label('id'), + 'pathway': rare_variant_pathway_association_1.pathway.label('pathway'), + 'pValue': rare_variant_pathway_association_1.p_value.label('p_value'), + 'min': rare_variant_pathway_association_1.min.label('min'), + 'max': rare_variant_pathway_association_1.max.label('max'), + 'mean': rare_variant_pathway_association_1.mean.label('mean'), + 'q1': rare_variant_pathway_association_1.q1.label('q1'), + 'q2': rare_variant_pathway_association_1.q2.label('q2'), + 'q3': rare_variant_pathway_association_1.q3.label('q3'), + 'nTotal': rare_variant_pathway_association_1.n_total.label('n_total'), + 'nMutants': rare_variant_pathway_association_1.n_mutants.label('n_mutants')} + data_set_core_field_mapping = { + 'display': data_set_1.display.label('data_set_display'), + 'name': data_set_1.name.label('data_set_name'), + 'type': data_set_1.dataset_type.label('data_set_type') + } + feature_core_field_mapping = { + 'display': feature_1.display.label('feature_display'), + 'name': feature_1.name.label('feature_name'), + 'order': feature_1.order.label('feature_order'), + 'unit': feature_1.unit.label('feature_unit'), + 'germlineModule': feature_1.germline_module.label('feature_germline_module'), + 'germlineCategory': feature_1.germline_category.label('feature_germline_category') + } + + core = get_selected(requested, core_field_mapping) + core |= get_selected(data_set_requested, data_set_core_field_mapping) + core |= get_selected(feature_requested, feature_core_field_mapping) + + query = sess.query(*core) + query = query.select_from(rare_variant_pathway_association_1) + + if pathway: + query = query.filter( + rare_variant_pathway_association_1.pathway.in_(pathway)) + + if max_p_value or max_p_value == 0: + query = query.filter( + rare_variant_pathway_association_1.p_value <= max_p_value) + + if min_p_value or min_p_value == 0: + query = query.filter( + rare_variant_pathway_association_1.p_value >= min_p_value) + + if 'dataSet' in requested or data_set: + is_outer = not bool(data_set) + data_set_join_condition = build_join_condition( + data_set_1.id, rare_variant_pathway_association_1.dataset_id, filter_column=data_set_1.name, filter_list=data_set) + query = query.join(data_set_1, and_( + *data_set_join_condition), isouter=is_outer) + + if 'feature' in requested or feature: + is_outer = not bool(feature) + data_set_join_condition = build_join_condition( + feature_1.id, rare_variant_pathway_association_1.feature_id, filter_column=feature_1.name, filter_list=feature) + query = query.join(feature_1, and_( + *data_set_join_condition), isouter=is_outer) + + return get_pagination_queries(query, paging, distinct, cursor_field=rare_variant_pathway_association_1.id) diff --git a/apps/iatlas/api-gitlab/api/resolvers/resolver_helpers/sample.py b/apps/iatlas/api-gitlab/api/resolvers/resolver_helpers/sample.py new file mode 100644 index 0000000000..ff43bcb7bf --- /dev/null +++ b/apps/iatlas/api-gitlab/api/resolvers/resolver_helpers/sample.py @@ -0,0 +1,234 @@ +from sqlalchemy import and_ +from sqlalchemy.orm import aliased +from api import db +from api.db_models import ( + Dataset, DatasetToSample, Feature, FeatureToSample, Patient, Sample +) +from .general_resolvers import build_join_condition, get_selected, get_value +from .paging_utils import get_pagination_queries + + +simple_sample_request_fields = {'name'} + +cohort_sample_request_fields = {'name', 'tag'} + +sample_request_fields = simple_sample_request_fields.union({'patient'}) + +feature_related_sample_request_fields = simple_sample_request_fields.union({ + 'value' +}) + +cell_type_feature_related_sample_request_fields = simple_sample_request_fields.union( + {'value', 'cellType'} +) + +gene_related_sample_request_fields = simple_sample_request_fields.union( + {'rnaSeqExpr', 'nanostringExpr'} +) + +cell_type_gene_related_sample_request_fields = simple_sample_request_fields.union( + {'cellType', 'singleCellSeqSum'} +) + +mutation_related_sample_request_fields = sample_request_fields.union({ + 'status'}) + +sample_by_mutation_status_request_fields = {'status', 'samples'} + + +def build_sample_graphql_response(prefix='sample_'): + from .patient import build_patient_graphql_response + from .tag import build_tag_graphql_response, has_tag_fields + + def f(sample): + if not sample: + return None + else: + dict = { + 'id': get_value(sample, prefix + 'id'), + 'name': get_value(sample, prefix + 'name'), + 'status': get_value(sample, prefix + 'mutation_status'), + 'rnaSeqExpr': get_value(sample, prefix + 'gene_rna_seq_expr'), + 'nanostringExpr': get_value(sample, prefix + 'gene_nanostring_expr'), + 'value': get_value(sample, prefix + 'feature_value'), + 'singleCellSeqSum': get_value(sample, prefix + 'single_seq_sum'), + 'cellType': get_value(sample, prefix + 'cell_type'), + 'patient': build_patient_graphql_response()(sample), + 'tag': build_tag_graphql_response()(sample) if has_tag_fields(sample) else None + } + return(dict) + return(f) + +def build_gene_expression_graphql_response(prefix='sample_'): + + def f(sample): + if not sample: + return None + else: + result_dict = { + 'id': get_value(sample, prefix + 'id'), + 'name': get_value(sample, prefix + 'name') + } + result_dict['rnaSeqExpr'] = get_value(sample, prefix + 'gene_rna_seq_expr') + result_dict['nanostringExpr'] = get_value(sample, prefix + 'gene_nanostring_expr') + return(result_dict) + return(f) + +def build_single_cell_seq_response(prefix='sample_'): + def f(sample): + if not sample: + return None + else: + result_dict = { + 'id': get_value(sample, prefix + 'id'), + 'name': get_value(sample, prefix + 'name'), + 'singleCellSeqSum': get_value(sample, prefix + 'single_cell_seq_sum'), + 'cellType': get_value(sample, prefix + 'cell_type') + } + return(result_dict) + return(f) + + +def build_sample_mutation_join_condition(sample_to_mutation_model, sample_model, mutation_status, mutation_id=None, status=None): + join_condition = build_join_condition(sample_to_mutation_model.sample_id, sample_model.id, + filter_column=sample_to_mutation_model.mutation_id, filter_list=mutation_id) + if mutation_status: + join_condition.append( + sample_to_mutation_model.status == mutation_status) + return join_condition + + +def build_sample_request( + requested, patient_requested, data_set=None, ethnicity=None, feature=None, feature_class=None, gender=None, max_age_at_diagnosis=None, max_height=None, max_weight=None, min_age_at_diagnosis=None, min_height=None, min_weight=None, patient=None, race=None, sample=None, distinct=False, paging=None): + ''' + Builds a SQL query. + + All positional arguments are required. Positional arguments are: + 1st position - a set of the requested fields at the root of the graphql request or in the 'samples' node if by mutation status or by tag. + 2nd position - a set of the requested fields in the 'patient' node of the graphql request. If 'patient' is not requested, this will be an empty set. + + All keyword arguments are optional. Keyword arguments are: + `data_set` - a list of strings, data set names + `ethnicity` - a list of strings, ethnicity enum + `feature` - a list of strings, feature names + `feature_class` - a list of strings, feature class names + `gender` - a list of strings, gender enum + `max_age_at_diagnosis` - an integer, a maximum age of a patient at the time of diagnosis + `max_height` - an integer, a maximum height of a patient + `max_weight` - an integer, a maximum weight of a patient + `min_age_at_diagnosis` - an integer, a minimum age of a patient at the time of diagnosis + `min_height` - an integer, a minimum height of a patient + `min_weight` - an integer, a minimum weight of a patient + `patient` - a list of strings, patient barcodes + `race` - a list of strings, race enum + `sample` - a list of strings, sample names + `distinct` - a boolean, indicates whether duplicate records should be filtered out + `paging` - a dict containing pagination metadata + ''' + sess = db.session + + has_patient_filters = bool( + patient or max_age_at_diagnosis or min_age_at_diagnosis or ethnicity or gender or max_height or min_height or race or max_weight or min_weight) + + data_set_to_sample_1 = aliased(DatasetToSample, name='ds') + patient_1 = aliased(Patient, name='p') + sample_1 = aliased(Sample, name='s') + + core_field_mapping = { + 'name': sample_1.name.label('sample_name') + } + patient_core_field_mapping = { + 'ageAtDiagnosis': patient_1.age_at_diagnosis.label('patient_age_at_diagnosis'), + 'barcode': patient_1.name.label('patient_barcode'), + 'ethnicity': patient_1.ethnicity.label('patient_ethnicity'), + 'gender': patient_1.gender.label('patient_gender'), + 'height': patient_1.height.label('patient_height'), + 'race': patient_1.race.label('patient_race'), + 'weight': patient_1.weight.label('patient_weight') + } + + core = get_selected(requested, core_field_mapping) + core.add(sample_1.id.label('sample_id')) + patient_core = get_selected(patient_requested, patient_core_field_mapping) + + query = sess.query(*[*core, *patient_core]) + query = query.select_from(sample_1) + + if sample: + query = query.filter(sample_1.name.in_(sample)) + + if has_patient_filters or 'patient' in requested: + is_outer = not has_patient_filters + + patient_join_condition = build_join_condition( + sample_1.patient_id, patient_1.id, patient_1.name, patient) + + if bool(max_age_at_diagnosis): + patient_join_condition.append( + patient_1.age_at_diagnosis <= max_age_at_diagnosis) + + if bool(min_age_at_diagnosis): + patient_join_condition.append( + patient_1.age_at_diagnosis >= min_age_at_diagnosis) + + if bool(ethnicity): + patient_join_condition.append(patient_1.ethnicity.in_(ethnicity)) + + if bool(gender): + patient_join_condition.append(patient_1.gender.in_(gender)) + + if bool(max_height): + patient_join_condition.append(patient_1.height <= max_height) + + if bool(min_height): + patient_join_condition.append(patient_1.height >= min_height) + + if bool(race): + patient_join_condition.append(patient_1.race.in_(race)) + + if bool(max_weight): + patient_join_condition.append(patient_1.weight <= max_weight) + + if bool(min_weight): + patient_join_condition.append(patient_1.weight >= min_weight) + + query = query.join(patient_1, and_( + *patient_join_condition), isouter=is_outer) + + if data_set: + data_set_1 = aliased(Dataset, name='d') + + data_set_sub_query = sess.query(data_set_1.id).filter( + data_set_1.name.in_(data_set)) if data_set else data_set + + data_set_to_sample_join_condition = build_join_condition( + data_set_to_sample_1.sample_id, sample_1.id, data_set_to_sample_1.dataset_id, data_set_sub_query) + query = query.join( + data_set_to_sample_1, and_(*data_set_to_sample_join_condition)) + + if feature or feature_class: + feature_1 = aliased(Feature, name='f') + feature_class_1 = aliased(FeatureClass, name='fc') + feature_to_sample_1 = aliased(FeatureToSample, name='fs') + + query = query.join(feature_to_sample_1, + feature_to_sample_1.sample_id == sample_1.id) + + feature_join_condition = build_join_condition( + feature_1.id, feature_to_sample_1.feature_id, feature_1.name, feature) + query = query.join(feature_1, and_(*feature_join_condition)) + + if feature_class: + feature_class_join_condition = build_join_condition( + feature_class_1.id, feature_1.class_id, feature_class_1.name, feature_class) + query = query.join( + feature_class_1, and_(*feature_class_join_condition)) + + order = [] + append_to_order = order.append + if 'name' in requested: + append_to_order(sample_1.name) + + query = query.order_by(*order) if order else query + + return get_pagination_queries(query, paging, distinct, cursor_field=sample_1.id) diff --git a/apps/iatlas/api-gitlab/api/resolvers/resolver_helpers/slide.py b/apps/iatlas/api-gitlab/api/resolvers/resolver_helpers/slide.py new file mode 100644 index 0000000000..3651fe46c1 --- /dev/null +++ b/apps/iatlas/api-gitlab/api/resolvers/resolver_helpers/slide.py @@ -0,0 +1,124 @@ +from sqlalchemy import and_ +from sqlalchemy.orm import aliased +from api import db +from api.db_models import Patient, Sample, Slide +from .general_resolvers import build_join_condition, get_selected, get_value +from .paging_utils import get_pagination_queries + + +simple_slide_request_fields = {'description', 'name'} + +slide_request_fields = simple_slide_request_fields.union({'patient'}) + + +def build_slide_graphql_response(prefix='slide_'): + from .patient import build_patient_graphql_response + + def f(slide): + if not slide: + return None + else: + dict = { + 'id': get_value(slide, prefix + 'id'), + 'description': get_value(slide, prefix + 'description'), + 'name': get_value(slide, prefix + 'name'), + 'patient': build_patient_graphql_response()(slide) + } + return(dict) + return(f) + + +def build_slide_request(requested, patient_requested, max_age_at_diagnosis=None, min_age_at_diagnosis=None, barcode=None, ethnicity=None, gender=None, max_height=None, min_height=None, + name=None, race=None, max_weight=None, min_weight=None, sample=None, distinct=False, paging=None): + """ + Builds a SQL query. + """ + sess = db.session + + has_patient_filters = bool( + barcode or max_age_at_diagnosis or min_age_at_diagnosis or ethnicity or gender or max_height or min_height or race or max_weight or min_weight) + + patient_1 = aliased(Patient, name='p') + sample_1 = aliased(Sample, name='s') + slide_1 = aliased(Slide, name='sd') + + core_field_mapping = { + 'description': slide_1.description.label('slide_description'), + 'name': slide_1.name.label('slide_name') + } + patient_core_field_mapping = { + 'ageAtDiagnosis': patient_1.age_at_diagnosis.label('patient_age_at_diagnosis'), + 'barcode': patient_1.name.label('patient_barcode'), + 'ethnicity': patient_1.ethnicity.label('patient_ethnicity'), + 'gender': patient_1.gender.label('patient_gender'), + 'height': patient_1.height.label('patient_height'), + 'race': patient_1.race.label('patient_race'), + 'weight': patient_1.weight.label('patient_weight') + } + + # Only select fields that were requested. + core = get_selected(requested, core_field_mapping) + core |= {slide_1.id.label('slide_id')} + + patient_core = get_selected(patient_requested, patient_core_field_mapping) + + query = sess.query(*[*core, *patient_core]) + query = query.select_from(slide_1) + + if name: + query = query.filter(slide_1.name.in_(name)) + + if has_patient_filters or 'patient' in requested: + is_outer = not has_patient_filters + + patient_join_condition = build_join_condition( + slide_1.patient_id, patient_1.id, patient_1.name, barcode) + + if bool(max_age_at_diagnosis): + patient_join_condition.append( + patient_1.age_at_diagnosis <= max_age_at_diagnosis) + + if bool(min_age_at_diagnosis): + patient_join_condition.append( + patient_1.age_at_diagnosis >= min_age_at_diagnosis) + + if bool(ethnicity): + patient_join_condition.append(patient_1.ethnicity.in_(ethnicity)) + + if bool(gender): + patient_join_condition.append(patient_1.gender.in_(gender)) + + if bool(max_height): + patient_join_condition.append(patient_1.height <= max_height) + + if bool(min_height): + patient_join_condition.append(patient_1.height >= min_height) + + if bool(race): + patient_join_condition.append(patient_1.race.in_(race)) + + if bool(max_weight): + patient_join_condition.append(patient_1.weight <= max_weight) + + if bool(min_weight): + patient_join_condition.append(patient_1.weight >= min_weight) + + query = query.join(patient_1, and_( + *patient_join_condition), isouter=is_outer) + + if sample: + sample_join_condition = build_join_condition( + slide_1.patient_id, sample_1.patient_id, filter_column=sample_1.name, filter_list=sample) + query = query.join(sample_1, and_( + *sample_join_condition), isouter=False) + + order = [] + append_to_order = order.append + if 'name' in requested: + append_to_order(slide_1.name) + if 'description' in requested: + append_to_order(slide_1.description) + + query = query.order_by(*order) if order else query + + return get_pagination_queries(query, paging, distinct, cursor_field=slide_1.id) diff --git a/apps/iatlas/api-gitlab/api/resolvers/resolver_helpers/snp.py b/apps/iatlas/api-gitlab/api/resolvers/resolver_helpers/snp.py new file mode 100644 index 0000000000..13ac121e59 --- /dev/null +++ b/apps/iatlas/api-gitlab/api/resolvers/resolver_helpers/snp.py @@ -0,0 +1,73 @@ +from sqlalchemy.orm import aliased +from api import db +from api.db_models import Snp +from .general_resolvers import get_selected, get_value +from .paging_utils import get_pagination_queries + +snp_request_fields = {'id', 'name', 'rsid', 'chr', 'bp'} + + +def build_snp_graphql_response(snp): + return { + 'id': get_value(snp, 'id'), + 'name': get_value(snp, 'snp_name') or get_value(snp), + 'rsid': get_value(snp, 'snp_rsid') or get_value(snp, 'rsid'), + 'chr': get_value(snp, 'snp_chr') or get_value(snp, 'chr'), + 'bp': get_value(snp, 'snp_bp') or get_value(snp, 'bp') + } + + +def build_snp_request( + requested, name=None, rsid=None, chr=None, max_bp=None, min_bp=None, distinct=False, paging=None): + """ + Builds a SQL request. + + All positional arguments are required. Positional arguments are: + 1st position - a set of the requested fields at the root of the graphql request + + All keyword arguments are optional. Keyword arguments are: + `name` - a list of strings + `rsid` - a list of strings + `chr` - a list of strings + `max_bp` - an integer + `min_bp` - an integer + `distinct` - a boolean, indicates whether duplicate records should be filtered out + `paging` - a dict containing pagination metadata + """ + sess = db.session + + snp_1 = aliased(Snp, name='snp') + + core_field_mapping = { + 'id': snp_1.id.label('id'), + 'name': snp_1.name.label('name'), + 'rsid': snp_1.rsid.label('rsid'), + 'chr': snp_1.chr.label('chr'), + 'bp': snp_1.bp.label('bp') + } + + core = get_selected(requested, core_field_mapping) + + if distinct == False: + # Add the id as a cursor if not selecting distinct + core |= {snp_1.id.label('id')} + + query = sess.query(*core) + query = query.select_from(snp_1) + + if name: + query = query.filter(snp_1.name.in_(name)) + + if rsid: + query = query.filter(snp_1.rsid.in_(rsid)) + + if chr: + query = query.filter(snp_1.chr.in_(chr)) + + if max_bp or max_bp == 0: + query = query.filter(snp_1.bp <= max_bp) + + if min_bp or min_bp == 0: + query = query.filter(snp_1.bp >= min_bp) + + return get_pagination_queries(query, paging, distinct, cursor_field=snp_1.id) diff --git a/apps/iatlas/api-gitlab/api/resolvers/resolver_helpers/tag.py b/apps/iatlas/api-gitlab/api/resolvers/resolver_helpers/tag.py new file mode 100644 index 0000000000..a8afeb8693 --- /dev/null +++ b/apps/iatlas/api-gitlab/api/resolvers/resolver_helpers/tag.py @@ -0,0 +1,309 @@ +from sqlalchemy import and_ +from sqlalchemy.orm import aliased +from api import db +from api.db_models import Dataset, DatasetToTag, Publication, Sample, SampleToTag, Tag, TagToPublication, TagToTag, Cohort, CohortToTag, CohortToSample +from .general_resolvers import build_join_condition, get_selected, get_value +from .paging_utils import get_pagination_queries + + +simple_tag_request_fields = { + 'characteristics', + 'color', + 'longDisplay', + 'name', + 'order', + 'shortDisplay', + 'tag', + 'type' +} + +tag_request_fields = simple_tag_request_fields.union({ + 'publications', + 'related', + 'sampleCount', + 'samples' +}) + + +def has_tag_fields(item, prefix='tag_'): + if not item: + return False + return(get_value(item, prefix + 'id') or get_value(item, prefix + 'name') or get_value( + item, prefix + 'characteristics') or get_value(item, prefix + 'short_display') or get_value(item, prefix + 'long_display') or get_value(item, prefix + 'type') or get_value(item, prefix + 'order')) + + +def build_tag_graphql_response(requested=[], sample_requested=[], publications_requested=[], related_requested=[], cohort=None, sample=None, prefix='tag_'): + from .publication import build_publication_graphql_response + from .sample import build_sample_graphql_response + + def f(tag): + if not tag: + return None + + tag_id = get_value(tag, prefix + 'id') + + sample_dict = get_samples( + tag_id=tag_id, requested=requested, sample_requested=sample_requested, cohort=cohort, sample=sample) + + publication_dict = get_publications( + tag_id=tag_id, requested=requested, publications_requested=publications_requested) + + related_dict = get_related( + tag_id=tag_id, requested=requested, related_requested=related_requested) + + result = { + 'id': tag_id, + 'name': get_value(tag, prefix + 'name') or get_value(tag, 'name'), + 'characteristics': get_value(tag, prefix + 'characteristics'), + 'color': get_value(tag, prefix + 'color'), + 'longDisplay': get_value(tag, prefix + 'long_display'), + 'shortDisplay': get_value(tag, prefix + 'short_display'), + 'type': get_value(tag, prefix + 'type'), + 'order': get_value(tag, prefix + 'order'), + 'sampleCount': len(sample_dict) if sample_dict and 'sampleCount' in requested else None, + 'publications': map(build_publication_graphql_response, publication_dict) if publication_dict else None, + 'related': map(build_tag_graphql_response(requested=related_requested), related_dict) if related_dict else None, + 'samples': map(build_sample_graphql_response(), sample_dict) if sample_dict and 'samples' in requested else None + } + return(result) + return(f) + + +def get_tag_column_labels(requested, tag, prefix='tag_', add_id=False): + mapping = { + 'characteristics': tag.description.label(prefix + 'characteristics'), + 'color': tag.color.label(prefix + 'color'), + 'longDisplay': tag.long_display.label(prefix + 'long_display'), + 'name': tag.name.label(prefix + 'name'), + 'order': tag.order.label(prefix + 'order'), + 'shortDisplay': tag.short_display.label(prefix + 'short_display'), + 'type': tag.tag_type.label(prefix + 'type'), + } + labels = get_selected(requested, mapping) + + if add_id: + labels |= {tag.id.label(prefix + 'id')} + + return(labels) + + +def build_tag_request(requested, distinct=False, paging=None, cohort=None, data_set=None, related=None, sample=None, tag=None, type=None): + + sess = db.session + + tag_1 = aliased(Tag, name='t') + sample_1 = aliased(Sample, name='s') + sample_to_tag_1 = aliased(SampleToTag, name='stt') + dataset_to_tag_1 = aliased(DatasetToTag, name='dtt') + dataset_1 = aliased(Dataset, name='d') + cohort_1 = aliased(Cohort, name='c') + cohort_to_tag_1 = aliased(CohortToTag, name='ctt') + tag_to_tag_1 = aliased(TagToTag, name='ttt') + + tag_core = get_tag_column_labels(requested, tag_1, add_id=True) + query = sess.query(*tag_core) + query = query.select_from(tag_1) + + if tag: + query = query.filter(tag_1.name.in_(tag)) + + if type: + query = query.filter(tag_1.tag_type.in_(type)) + + if data_set: + dataset_subquery = sess.query(dataset_to_tag_1.tag_id) + + dataset_join_condition = build_join_condition( + dataset_to_tag_1.dataset_id, dataset_1.id, filter_column=dataset_1.name, filter_list=data_set) + dataset_subquery = dataset_subquery.join(dataset_1, and_( + *dataset_join_condition), isouter=False) + + query = query.filter(tag_1.id.in_(dataset_subquery)) + + if cohort: + cohort_subquery = sess.query(cohort_to_tag_1.tag_id) + + cohort_join_condition = build_join_condition( + cohort_to_tag_1.cohort_id, cohort_1.id, filter_column=cohort_1.name, filter_list=cohort) + cohort_subquery = cohort_subquery.join(cohort_1, and_( + *cohort_join_condition), isouter=False) + + query = query.filter(tag_1.id.in_(cohort_subquery)) + + if related: + related_subquery = sess.query(tag_to_tag_1.tag_id) + + related_join_condition = build_join_condition( + tag_to_tag_1.related_tag_id, tag_1.id, filter_column=tag_1.name, filter_list=related) + related_subquery = related_subquery.join(tag_1, and_( + *related_join_condition), isouter=False) + + query = query.filter(tag_1.id.in_(related_subquery)) + + if sample: + sample_subquery = sess.query(sample_to_tag_1.tag_id) + + sample_join_condition = build_join_condition( + sample_to_tag_1.sample_id, sample_1.id, filter_column=sample_1.name, filter_list=sample) + sample_subquery = sample_subquery.join(sample_1, and_( + *sample_join_condition), isouter=False) + + query = query.filter(tag_1.id.in_(sample_subquery)) + + order = [] + append_to_order = order.append + if 'name' in requested: + append_to_order(tag_1.name) + if 'shortDisplay' in requested: + append_to_order(tag_1.short_display) + if 'longDisplay' in requested: + append_to_order(tag_1.long_display) + if 'color' in requested: + append_to_order(tag_1.color) + if 'characteristics' in requested: + append_to_order(tag_1.description) + + query = query.order_by(*order) if order else query + + return get_pagination_queries(query, paging, distinct, cursor_field=tag_1.id) + + +def get_publications(tag_id, requested, publications_requested): + if 'publications' in requested: + sess = db.session + + pub_1 = aliased(Publication, name='p') + tag_1 = aliased(Tag, name='t') + tag_to_pub_1 = aliased(TagToPublication, name='tp') + + core_field_mapping = { + 'doId': pub_1.do_id.label('do_id'), + 'firstAuthorLastName': pub_1.first_author_last_name.label('first_author_last_name'), + 'journal': pub_1.journal.label('journal'), + 'name': pub_1.title.label('name'), + 'pubmedId': pub_1.pubmed_id.label('pubmed_id'), + 'title': pub_1.title.label('title'), + 'year': pub_1.year.label('year') + } + + core = get_selected(publications_requested, core_field_mapping) + # Always select the publication id and the tag id. + core |= { + pub_1.id.label('id'), + tag_to_pub_1.tag_id.label('tag_id') + } + + pub_query = sess.query(*core) + pub_query = pub_query.select_from(pub_1) + + tag_sub_query = sess.query(tag_1.id).filter(tag_1.id.in_([tag_id])) + + tag_tag_join_condition = build_join_condition( + tag_to_pub_1.publication_id, pub_1.id, tag_to_pub_1.tag_id, tag_sub_query) + pub_query = pub_query.join( + tag_to_pub_1, and_(*tag_tag_join_condition)) + + order = [] + append_to_order = order.append + if 'name' in publications_requested: + append_to_order(pub_1.title) + if 'pubmedId' in publications_requested: + append_to_order(pub_1.pubmed_id) + if 'doId' in publications_requested: + append_to_order(pub_1.do_id) + if 'title' in publications_requested: + append_to_order(pub_1.title) + if 'firstAuthorLastName' in publications_requested: + append_to_order(pub_1.first_author_last_name) + if 'year' in publications_requested: + append_to_order(pub_1.year) + if 'journal' in publications_requested: + append_to_order(pub_1.journal) + pub_query = pub_query.order_by(*order) if order else pub_query + + return pub_query.distinct().all() + + return [] + + +def get_related(tag_id, requested, related_requested): + if 'related' in requested: + sess = db.session + + tag_1 = aliased(Tag, name='t') + tag_to_tag_1 = aliased(TagToTag, name='ttt') + related_tag_1 = aliased(Tag, name='rt') + + related_core_field_mapping = { + 'characteristics': related_tag_1.description.label('tag_characteristics'), + 'color': related_tag_1.color.label('tag_color'), + 'longDisplay': related_tag_1.long_display.label('tag_long_display'), + 'name': related_tag_1.name.label('tag_name'), + 'order': related_tag_1.order.label('tag_order'), + 'shortDisplay': related_tag_1.short_display.label('tag_short_display'), + 'type': related_tag_1.tag_type.label('tag_type'), + } + + related_core = get_selected( + related_requested, related_core_field_mapping) + + related_query = sess.query(*related_core) + related_query = related_query.select_from(related_tag_1) + + tag_sub_query = sess.query(tag_to_tag_1.related_tag_id) + + tag_tag_join_condition = build_join_condition( + tag_1.id, tag_to_tag_1.tag_id, tag_1.id, [tag_id]) + + tag_sub_query = tag_sub_query.join( + tag_1, and_(*tag_tag_join_condition)) + + related_query = related_query.filter( + related_tag_1.id.in_(tag_sub_query)) + + return related_query.distinct().all() + + return [] + + +def get_samples(tag_id, requested, sample_requested, cohort=None, sample=None): + if 'samples' in requested or 'sampleCount' in requested: + sess = db.session + + sample_1 = aliased(Sample, name='s') + sample_to_tag_1 = aliased(SampleToTag, name='stt') + cohort_1 = aliased(Cohort, name='c') + cohort_to_sample_1 = aliased(CohortToSample, name='cts') + + sample_core_field_mapping = { + 'name': sample_1.name.label('sample_name')} + + sample_core = get_selected(sample_requested, sample_core_field_mapping) + sample_core |= {sample_1.id.label('sample_id')} + + sample_query = sess.query(*sample_core) + sample_query = sample_query.select_from(sample_1) + + tag_subquery = sess.query( + sample_to_tag_1.sample_id).filter(sample_to_tag_1.tag_id.in_([tag_id])) + + sample_query = sample_query.filter( + sample_1.id.in_(tag_subquery)) + + if sample: + sample_query = sample_query.filter(sample_1.name.in_(sample)) + + if cohort: + cohort_subquery = sess.query(cohort_to_sample_1.sample_id) + + cohort_join_condition = build_join_condition( + cohort_to_sample_1.cohort_id, cohort_1.id, filter_column=cohort_1.name, filter_list=cohort) + cohort_subquery = cohort_subquery.join(cohort_1, and_( + *cohort_join_condition), isouter=False) + + sample_query = sample_query.filter( + sample_1.id.in_(cohort_subquery)) + + return sample_query.distinct().all() + + return [] diff --git a/apps/iatlas/api-gitlab/api/resolvers/samples_resolver.py b/apps/iatlas/api-gitlab/api/resolvers/samples_resolver.py new file mode 100644 index 0000000000..1b8f14bfc8 --- /dev/null +++ b/apps/iatlas/api-gitlab/api/resolvers/samples_resolver.py @@ -0,0 +1,21 @@ +from .resolver_helpers import get_requested, build_sample_graphql_response, build_sample_request, simple_patient_request_fields, sample_request_fields, get_selection_set +from .resolver_helpers.paging_utils import paginate, Paging, paging_fields + + +def resolve_samples(_obj, info, maxAgeAtDiagnosis=None, minAgeAtDiagnosis=None, ethnicity=None, gender=None, maxHeight=None, minHeight=None, name=None, patient=None, race=None, maxWeight=None, minWeight=None, paging=None, distinct=False): + + selection_set = get_selection_set(info=info, child_node='items') + + requested = get_requested( + selection_set=selection_set, requested_field_mapping=sample_request_fields) + + patient_requested = get_requested( + selection_set=selection_set, requested_field_mapping=simple_patient_request_fields, child_node='patient') + + paging = paging if paging else Paging.DEFAULT + + query, count_query = build_sample_request(requested, patient_requested, max_age_at_diagnosis=maxAgeAtDiagnosis, min_age_at_diagnosis=minAgeAtDiagnosis, + ethnicity=ethnicity, gender=gender, max_height=maxHeight, min_height=minHeight, patient=patient, race=race, sample=name, max_weight=maxWeight, min_weight=minWeight, paging=paging, distinct=distinct) + + pagination_requested = get_requested(info, paging_fields, 'paging') + return paginate(query, count_query, paging, distinct, build_sample_graphql_response(), pagination_requested) diff --git a/apps/iatlas/api-gitlab/api/resolvers/slide_resolver.py b/apps/iatlas/api-gitlab/api/resolvers/slide_resolver.py new file mode 100644 index 0000000000..0d6af70d1b --- /dev/null +++ b/apps/iatlas/api-gitlab/api/resolvers/slide_resolver.py @@ -0,0 +1,22 @@ +from .resolver_helpers import build_slide_graphql_response, get_requested, simple_patient_request_fields, slide_request_fields, get_selection_set, build_slide_request +from .resolver_helpers.paging_utils import paginate, Paging, paging_fields + + +def resolve_slides(_obj, info, maxAgeAtDiagnosis=None, minAgeAtDiagnosis=None, barcode=None, ethnicity=None, gender=None, maxHeight=None, minHeight=None, name=None, race=None, maxWeight=None, minWeight=None, sample=None, paging=None, distinct=False): + + selection_set = get_selection_set(info=info, child_node='items') + + requested = get_requested( + selection_set=selection_set, requested_field_mapping=slide_request_fields) + + patient_requested = get_requested( + selection_set=selection_set, requested_field_mapping=simple_patient_request_fields, child_node='patient') + + paging = paging if paging else Paging.DEFAULT + + query, count_query = build_slide_request( + requested, patient_requested, max_age_at_diagnosis=maxAgeAtDiagnosis, min_age_at_diagnosis=minAgeAtDiagnosis, barcode=barcode, + ethnicity=ethnicity, gender=gender, max_height=maxHeight, min_height=minHeight, name=name, race=race, max_weight=maxWeight, min_weight=minWeight, sample=sample, paging=paging, distinct=distinct) + + pagination_requested = get_requested(info, paging_fields, 'paging') + return paginate(query, count_query, paging, distinct, build_slide_graphql_response(), pagination_requested) diff --git a/apps/iatlas/api-gitlab/api/resolvers/snp_resolver.py b/apps/iatlas/api-gitlab/api/resolvers/snp_resolver.py new file mode 100644 index 0000000000..dd53f4d90f --- /dev/null +++ b/apps/iatlas/api-gitlab/api/resolvers/snp_resolver.py @@ -0,0 +1,16 @@ +from .resolver_helpers import ( + get_requested, snp_request_fields, build_snp_graphql_response, build_snp_request) + +from .resolver_helpers.paging_utils import paginate, Paging, paging_fields + + +def resolve_snps(_obj, info, name=None, rsid=None, chr=None, maxBP=None, minBP=None, paging=None, distinct=False): + requested = get_requested(info, snp_request_fields, "items") + + paging = paging if paging else Paging.DEFAULT + + query, count_query = build_snp_request( + requested, name=name, rsid=rsid, chr=chr, max_bp=maxBP, min_bp=minBP, paging=paging, distinct=distinct) + + pagination_requested = get_requested(info, paging_fields, 'paging') + return paginate(query, count_query, paging, distinct, build_snp_graphql_response, pagination_requested) diff --git a/apps/iatlas/api-gitlab/api/resolvers/tags_resolver.py b/apps/iatlas/api-gitlab/api/resolvers/tags_resolver.py new file mode 100644 index 0000000000..326207bcc9 --- /dev/null +++ b/apps/iatlas/api-gitlab/api/resolvers/tags_resolver.py @@ -0,0 +1,39 @@ +from .resolver_helpers import build_tag_graphql_response, build_tag_request, get_requested, simple_sample_request_fields, simple_publication_request_fields, simple_tag_request_fields, tag_request_fields, get_selection_set +from .resolver_helpers.paging_utils import paginate, paging_fields, create_paging + + +def resolve_tags(_obj, info, distinct=False, paging=None, cohort=None, dataSet=None, related=None, sample=None, tag=None, type=None): + + selection_set = get_selection_set(info=info, child_node='items') + + requested = get_requested( + selection_set=selection_set, requested_field_mapping=tag_request_fields) + + sample_requested = get_requested( + selection_set=selection_set, requested_field_mapping=simple_sample_request_fields, child_node='samples') + + publications_requested = get_requested( + selection_set=selection_set, requested_field_mapping=simple_publication_request_fields, child_node='publications') + + related_requested = get_requested( + selection_set=selection_set, requested_field_mapping=simple_tag_request_fields, child_node='related') + + max_items = 10 if 'samples' in requested else 100_000 + + paging = create_paging(paging, max_items) + + query, count_query = build_tag_request( + requested, distinct=distinct, paging=paging, cohort=cohort, data_set=dataSet, related=related, sample=sample, tag=tag, type=type) + + pagination_requested = get_requested(info, paging_fields, 'paging') + + res = paginate( + query, + count_query, + paging, + distinct, + build_tag_graphql_response(requested, sample_requested, publications_requested, related_requested, cohort=cohort, sample=sample), + pagination_requested + ) + + return(res) diff --git a/apps/iatlas/api-gitlab/api/resolvers/test_resolver.py b/apps/iatlas/api-gitlab/api/resolvers/test_resolver.py new file mode 100644 index 0000000000..d0e4975e6f --- /dev/null +++ b/apps/iatlas/api-gitlab/api/resolvers/test_resolver.py @@ -0,0 +1,21 @@ +def resolve_test(_obj, info): + request = info.context + headers = request.headers + content_length = headers.get('Content-Length') + content_type = headers.get('Content-Type') + host = headers.get('Host') + referer = headers.get('Referer') + user_agent = headers.get('User-Agent') + return { + 'items': { + 'contentType': content_type, + 'userAgent': user_agent, + 'headers': { + 'contentLength': content_length, + 'contentType': content_type, + 'host': host, + 'userAgent': user_agent + } + }, + 'page': 1 + } diff --git a/apps/iatlas/api-gitlab/api/routes.py b/apps/iatlas/api-gitlab/api/routes.py new file mode 100644 index 0000000000..7534ef2406 --- /dev/null +++ b/apps/iatlas/api-gitlab/api/routes.py @@ -0,0 +1,48 @@ +from ariadne import graphql_sync +from ariadne.constants import PLAYGROUND_HTML +from ariadne.contrib.tracing.apollotracing import ApolloTracingExtensionSync +from flask import current_app, jsonify, request +import os +from .main import bp +from .schema import schema + + +@bp.route('/graphiql', methods=['GET']) +def graphql_playgroud(): + # On GET request serve GraphQL Playground + # You don't need to provide Playground if you don't want to + # but keep on mind this will not prohibit clients from + # exploring your API using desktop GraphQL Playground app. + return PLAYGROUND_HTML, 200 + + +@bp.route('/graphiql', methods=['POST']) +@bp.route('/api', methods=['POST']) +def graphql_server(): + # GraphQL queries are always sent as POST + data = request.get_json() + + # By default, no extensions. + # If the FLASK_ENV environment variable is set to something + # other than 'production', enable Apollo Tracing. + extensions = None + if ('FLASK_ENV' in os.environ and os.environ['FLASK_ENV'] != 'production'): + extensions = [ApolloTracingExtensionSync] + + # Note: Passing the request to the context is optional. + # In Flask, the current request is always accessible as flask.request + success, result = graphql_sync( + schema, + data, + context_value=request, + debug=current_app.debug, + extensions=extensions + ) + + status_code = 200 if success else 400 + return jsonify(result), status_code + + +@bp.route('/healthcheck') +def healthcheck(): + return 'Running', 200 diff --git a/apps/iatlas/api-gitlab/api/schema/__init__.py b/apps/iatlas/api-gitlab/api/schema/__init__.py new file mode 100644 index 0000000000..449820fcb2 --- /dev/null +++ b/apps/iatlas/api-gitlab/api/schema/__init__.py @@ -0,0 +1,298 @@ +from ariadne import load_schema_from_path, make_executable_schema, ObjectType, ScalarType +import os +from api.resolvers import ( + resolve_cell_stats, + resolve_cells, + resolve_cohorts, + resolve_colocalizations, + resolve_copy_number_results, + resolve_data_sets, + resolve_driver_results, + resolve_edges, + resolve_features, + resolve_gene_types, + resolve_genes, + resolve_germline_gwas_results, + resolve_heritability_results, + resolve_mutations, + resolve_mutation_types, + resolve_neoantigens, + resolve_nodes, + resolve_rare_variant_pathway_associations, + resolve_patients, + resolve_samples, + resolve_slides, + resolve_snps, + resolve_tags, + resolve_test, + resolve_heritability_results +) + + +schema_dirname, _filename = os.path.split(os.path.abspath(__file__)) + +# Import GraphQl schemas/ +root_query = load_schema_from_path(schema_dirname + '/root.query.graphql') +paging_types = load_schema_from_path( + schema_dirname + '/paging.graphql') +cell_query = load_schema_from_path( + schema_dirname + '/cell.query.graphql') +cell_stat_query = load_schema_from_path( + schema_dirname + '/cellStat.query.graphql') +cohort_query = load_schema_from_path( + schema_dirname + '/cohort.query.graphql') +colocalization_query = load_schema_from_path( + schema_dirname + '/colocalization.query.graphql') +copy_number_result_query = load_schema_from_path( + schema_dirname + '/copyNumberResult.query.graphql') +data_set_query = load_schema_from_path( + schema_dirname + '/dataset.query.graphql') +driver_result_query = load_schema_from_path( + schema_dirname + '/driverResult.query.graphql') +edge_query = load_schema_from_path( + schema_dirname + '/edge.query.graphql') +feature_query = load_schema_from_path( + schema_dirname + '/feature.query.graphql') +gene_query = load_schema_from_path(schema_dirname + '/gene.query.graphql') +gene_type_query = load_schema_from_path( + schema_dirname + '/geneType.query.graphql') +germline_gwas_result_query = load_schema_from_path( + schema_dirname + '/germlineGwasResult.query.graphql') +heritability_result_query = load_schema_from_path( + schema_dirname + '/heritabilityResult.query.graphql') +mutation_query = load_schema_from_path( + schema_dirname + '/mutation.query.graphql') +neoantigen_query = load_schema_from_path( + schema_dirname + '/neoantigen.query.graphql') +node_query = load_schema_from_path( + schema_dirname + '/node.query.graphql') +patient_query = load_schema_from_path( + schema_dirname + '/patient.query.graphql') +publication_query = load_schema_from_path( + schema_dirname + '/publication.query.graphql') +rare_variant_pathway_association_query = load_schema_from_path( + schema_dirname + '/rareVariantPathwayAssociation.query.graphql') +sample_query = load_schema_from_path(schema_dirname + '/sample.query.graphql') +slide_query = load_schema_from_path(schema_dirname + '/slide.query.graphql') +snp_query = load_schema_from_path(schema_dirname + '/snp.query.graphql') +tag_query = load_schema_from_path(schema_dirname + '/tag.query.graphql') + +type_defs = [ + root_query, + paging_types, + cell_query, + cell_stat_query, + cohort_query, + colocalization_query, + copy_number_result_query, + data_set_query, + driver_result_query, + edge_query, + feature_query, + gene_query, + gene_type_query, + germline_gwas_result_query, + heritability_result_query, + neoantigen_query, + mutation_query, + node_query, + rare_variant_pathway_association_query, + patient_query, + publication_query, + sample_query, + slide_query, + snp_query, + tag_query +] + +# Initialize custom scalars. +direction_enum_scalar = ScalarType('DirectionEnum') + + +@direction_enum_scalar.serializer +def serialize_direction_enum(value): + return value if value == 'Amp' or value == 'Del' else None + + +ethnicity_enum_scalar = ScalarType('EthnicityEnum') + + +@ethnicity_enum_scalar.serializer +def serialize_ethnicity_enum(value): + return value if value == 'not hispanic or latino' or value == 'hispanic or latino' else None + + +gender_enum_scalar = ScalarType('GenderEnum') + + +@gender_enum_scalar.serializer +def serialize_gender_enum(value): + return value if value == 'female' or value == 'male' else None + + +race_enum_scalar = ScalarType('RaceEnum') + + +@race_enum_scalar.serializer +def serialize_race_enum(value): + race_set = { + 'american indian or alaska native', + 'native hawaiian or other pacific islander', + 'black or african american', + 'asian', + 'white' + } + return value if value in race_set else None + + +status_enum_scalar = ScalarType('StatusEnum') + + +@status_enum_scalar.serializer +def serialize_status_enum(value): + return value if value == 'Mut' or value == 'Wt' else None + + +qtl_type_enum = ScalarType('QTLTypeEnum') + + +@qtl_type_enum.serializer +def serialize_qtl_type_enum(value): + return value if value == 'sQTL' or value == 'eQTL' else None + + +ecaviar_pp_enum = ScalarType('ECaviarPPEnum') + + +@ecaviar_pp_enum.serializer +def serialize_ecaviar_pp_enum(value): + return value if value == 'C1' or value == 'C2' else None + + +coloc_plot_type_enum = ScalarType('ColocPlotTypeEnum') + + +@coloc_plot_type_enum.serializer +def serialize_coloc_plot_type_enum(value): + return value if value == '3 Level Plot' or value == 'Expanded Region' else None + + +# Initialize schema objects (general). +root = ObjectType('Query') +cell = ObjectType('Cell') +cell_stat = ObjectType('CellStat') +cohort = ObjectType('Cohort') +colocalization = ObjectType('Colocalization') +copy_number_result = ObjectType('CopyNumberResult') +data_set = ObjectType('DataSet') +driver_result = ObjectType('DriverResult') +edge_result = ObjectType('EdgeResult') +feature = ObjectType('Feature') +gene = ObjectType('Gene') +gene_type = ObjectType('GeneType') +germline_gwas_result_node = ObjectType('GermlineGwasResultNode') +germline_gwas_result = ObjectType('GermlineGwasResult') +heritability_result_node = ObjectType('HeritabilityResultNode') +heritability_result = ObjectType('HeritabilityResult') +mutation = ObjectType('Mutation') +mutation_type = ObjectType('MutationType') +neoantigen = ObjectType('Neoantigen') +node = ObjectType('Node') +node_result = ObjectType('NodeResult') +patient = ObjectType('Patient') +publication = ObjectType('Publication') +rare_variant_pathway_association = ObjectType( + 'RareVariantPathwayAssociationNode') +related_by_data_set = ObjectType('RelatedByDataSet') +sample = ObjectType('Sample') +sample_by_mutation_status = ObjectType('SampleByMutationStatus') +slide = ObjectType('Slide') +snp = ObjectType('Snp') +tag = ObjectType('Tag') + +# Initialize schema objects (simple). +simple_data_set = ObjectType('SimpleDataSet') +simple_feature = ObjectType('SimpleFeature') +simple_gene = ObjectType('SimpleGene') +simple_gene_type = ObjectType('SimpleGeneType') +simple_node = ObjectType('SimpleNode') +simple_publication = ObjectType('SimplePublication') +simple_tag = ObjectType('SimpleTag') + +''' +Associate resolvers with fields. +Fields should be names of objects in schema/root.query.graphql. +Values should be names of functions in resolvers +''' +root.set_field('cells', resolve_cells) +root.set_field('cellStats', resolve_cell_stats) +root.set_field('cohorts', resolve_cohorts) +root.set_field('colocalizations', resolve_colocalizations) +root.set_field('copyNumberResults', resolve_copy_number_results) +root.set_field('dataSets', resolve_data_sets) +root.set_field('driverResults', resolve_driver_results) +root.set_field('edges', resolve_edges) +root.set_field('features', resolve_features) +root.set_field('geneTypes', resolve_gene_types) +root.set_field('genes', resolve_genes) +root.set_field('germlineGwasResults', resolve_germline_gwas_results) +root.set_field('heritabilityResults', resolve_heritability_results) +root.set_field('mutations', resolve_mutations) +root.set_field('mutationTypes', resolve_mutation_types) +root.set_field('neoantigens', resolve_neoantigens) +root.set_field('nodes', resolve_nodes) +root.set_field('patients', resolve_patients) +root.set_field('rareVariantPathwayAssociations', + resolve_rare_variant_pathway_associations) +root.set_field('samples', resolve_samples) +root.set_field('slides', resolve_slides) +root.set_field('snps', resolve_snps) +root.set_field('tags', resolve_tags) +root.set_field('test', resolve_test) + + +schema = make_executable_schema( + type_defs, + [ + root, + cell, + cell_stat, + cohort, + colocalization, + copy_number_result, + data_set, + direction_enum_scalar, + driver_result, + edge_result, + ethnicity_enum_scalar, + feature, gender_enum_scalar, + gene, + gene_type, + germline_gwas_result, + germline_gwas_result_node, + heritability_result_node, + heritability_result, + mutation, + mutation_type, + neoantigen, + node, + node_result, + patient, + publication, + race_enum_scalar, + rare_variant_pathway_association, + related_by_data_set, + sample, + sample_by_mutation_status, + simple_data_set, + simple_feature, + simple_gene, + simple_gene_type, + simple_node, + simple_publication, + simple_tag, + slide, + snp, + tag + ] +) diff --git a/apps/iatlas/api-gitlab/api/schema/cell.query.graphql b/apps/iatlas/api-gitlab/api/schema/cell.query.graphql new file mode 100644 index 0000000000..3fe28847ca --- /dev/null +++ b/apps/iatlas/api-gitlab/api/schema/cell.query.graphql @@ -0,0 +1,32 @@ +""" +The "CellNode" type +""" +type CellNode implements BaseNode { + "A unique id for the database. Please note that this value may change as the database gets updated and should not be relied on." + id: ID + "The type of cell the stats are for." + type: String! + "The name of the cell" + name: String! + "A list of features that the cell has a value" + features: [CellRelatedFeature!]! +} + +type Cell implements BaseResult { + "A Paging object (see Paging)" + paging: Paging + "A string describing any error that may have occurred." + error: String + "A list of returned CellNodes" + items: [CellNode] +} + +""" +The "CellRelatedFeature" is a version of a `Feature` used by `Cell`. +""" +type CellRelatedFeature { + "The features name." + name: String! + "The value of the feature for this cell" + value: Float! +} diff --git a/apps/iatlas/api-gitlab/api/schema/cellStat.query.graphql b/apps/iatlas/api-gitlab/api/schema/cellStat.query.graphql new file mode 100644 index 0000000000..c0c4d19480 --- /dev/null +++ b/apps/iatlas/api-gitlab/api/schema/cellStat.query.graphql @@ -0,0 +1,26 @@ +""" +The "CellStatNode" type +""" +type CellStatNode implements BaseNode { + "A unique id for the database. Please note that this value may change as the database gets updated and should not be relied on." + id: ID + "The the type of cell the stats are for." + type: String! + "The number of cells making up the group" + count: Int + avgExpr: Float + percExpr: Float + "The associated dataset" + dataSet: SimpleDataSet! + "The associated gene" + gene: SimpleGene! +} + +type CellStat implements BaseResult { + "A Paging object (see Paging)" + paging: Paging + "A string describing any error that may have occurred." + error: String + "A list of returned CellStatNodes" + items: [CellStatNode] +} \ No newline at end of file diff --git a/apps/iatlas/api-gitlab/api/schema/cohort.query.graphql b/apps/iatlas/api-gitlab/api/schema/cohort.query.graphql new file mode 100644 index 0000000000..3725c3719c --- /dev/null +++ b/apps/iatlas/api-gitlab/api/schema/cohort.query.graphql @@ -0,0 +1,31 @@ + +""" +The "CohortNode" type +""" +type CohortNode implements BaseNode { + "A unique id for the data set generated by the database. Please note that this value may change as the database gets updated and should not be relied on." + id: ID + "The name of the cohort." + name: String! + "The data set associated with the cohort." + dataSet: SimpleDataSet! + "The tag associated with the cohort." + tag: SimpleTag + "The samples associated with the cohort." + samples: [CohortSample!]! + "The features associated with the cohort." + features: [SimpleFeature!]! + "The genes associated with the cohort." + genes: [SimpleGene!]! + "The mutations associated with the cohort." + mutations: [SimpleMutation!]! +} + +type Cohort implements BaseResult { + "A Paging object (see Paging)" + paging: Paging + "A string describing any error that may have occurred." + error: String + "A list of returned CohortNodes" + items: [CohortNode] +} diff --git a/apps/iatlas/api-gitlab/api/schema/colocalization.query.graphql b/apps/iatlas/api-gitlab/api/schema/colocalization.query.graphql new file mode 100644 index 0000000000..ed716b864b --- /dev/null +++ b/apps/iatlas/api-gitlab/api/schema/colocalization.query.graphql @@ -0,0 +1,53 @@ +""" +'sQTL' or 'eQTL' +""" +scalar QTLTypeEnum + +""" +'C1' or 'C2' +""" +scalar ECaviarPPEnum + +""" +'3 Level Plot' or 'Expanded Region' +""" +scalar ColocPlotTypeEnum + +""" +The "ColocalizationNode" type +""" +type ColocalizationNode implements BaseNode { + "A unique id for the data set generated by the database. Please note that this value may change as the database gets updated and should not be relied on." + id: ID + "The QTL type of the colocalization to filter by. (either 'sQTL' or 'eQTL')." + qtlType: QTLTypeEnum! + "The eCaviar colocalization posterior probability locus model. (either 'C1' or 'C2')." + eCaviarPP: ECaviarPPEnum + "Type of colocalization plot. (either '3 Level Plot' or 'Expanded Region')." + plotType: ColocPlotTypeEnum + "Source tissue for QTL association, for gTEX colocalizations." + tissue: String + "For sQTLS, the location of the splice event." + spliceLoc: String + "A link to the associated plot." + plotLink: String + "The data set associated with the colocalization." + dataSet: SimpleDataSet! + "The data set whose genes were used to compute the colocalization." + colocDataSet: SimpleDataSet! + "The feature associated with the colocalization." + feature: SimpleFeature! + "The gene associated with the colocalization." + gene: SimpleGene! + "The snp associated with the colocalization." + snp: SnpNode! +} + +type Colocalization implements BaseResult { + "A Paging object (see Paging)" + paging: Paging + "A string describing any error that may have occurred." + error: String + "A list of returned ColocalizationNodes" + items: [ColocalizationNode] +} diff --git a/apps/iatlas/api-gitlab/api/schema/copyNumberResult.query.graphql b/apps/iatlas/api-gitlab/api/schema/copyNumberResult.query.graphql new file mode 100644 index 0000000000..9109d89f63 --- /dev/null +++ b/apps/iatlas/api-gitlab/api/schema/copyNumberResult.query.graphql @@ -0,0 +1,41 @@ +""" +The "DirectionEnum" scalar will always be either `Amp`, `Del`. +""" +scalar DirectionEnum + +""" +The "CopyNumberResultNode" type +""" +type CopyNumberResultNode implements BaseNode { + "A unique id for the data set generated by the database. Please note that this value may change as the database gets updated and should not be relied on." + id: ID + "The direction of the copy number result (either 'Amp' or 'Del')." + direction: DirectionEnum! + "The mean normal value of the copy number result." + meanNormal: Float + "The mean CNV value of the copy number result." + meanCnv: Float + "The P value of the copy number result." + pValue: Float + "The log10 computed result of the P value." + log10PValue: Float + "The T Stat value of the copy number result." + tStat: Float + "The data set associated with the copy number result." + dataSet: SimpleDataSet! + "The feature associated with the copy number result." + feature: SimpleFeature! + "The gene associated with the copy number result." + gene: SimpleGene! + "The tag associated with the copy number result." + tag: SimpleTag! +} + +type CopyNumberResult implements BaseResult { + "A Paging object (see Paging)" + paging: Paging + "A string describing any error that may have occurred." + error: String + "A list of returned CopyNumberResultNodes" + items: [CopyNumberResultNode] +} diff --git a/apps/iatlas/api-gitlab/api/schema/dataset.query.graphql b/apps/iatlas/api-gitlab/api/schema/dataset.query.graphql new file mode 100644 index 0000000000..400087e92c --- /dev/null +++ b/apps/iatlas/api-gitlab/api/schema/dataset.query.graphql @@ -0,0 +1,38 @@ +""" +The "Dataset" type +""" +type DataSetNode implements BaseNode { + "A unique id for the driver result generated by the database. Please note that this value may change as the database gets updated and should not be relied on." + id: ID! + "The 'name' of the data set." + name: String! + "A friendly name of the data set." + display: String + "A list of samples associated with the data set." + samples: [SimpleSample!] + "A list of samples associated with the data set." + tags: [SimpleTag!] + "The type of data set this is." + type: String +} + +type DataSet implements BaseResult { + "A Paging object (see Paging)" + paging: Paging + "A string describing any error that may have occurred." + error: String + "A list of returned DatasetNodes" + items: [DataSetNode] +} + +""" +The "SimpleDataSet" type is a version of a DataSet. Only basic attributes may be returned. +""" +type SimpleDataSet { + "The proper 'name' of the data set." + name: String! + "A friendly name of the data set." + display: String + "The type of data set this is." + type: String +} diff --git a/apps/iatlas/api-gitlab/api/schema/driverResult.query.graphql b/apps/iatlas/api-gitlab/api/schema/driverResult.query.graphql new file mode 100644 index 0000000000..09c75efa4b --- /dev/null +++ b/apps/iatlas/api-gitlab/api/schema/driverResult.query.graphql @@ -0,0 +1,36 @@ +""" +The "DriverResultNode" type +""" +type DriverResultNode implements BaseNode { + "A unique id for the driver result generated by the database. Please note that this value may change as the database gets updated and should not be relied on." + id: ID + "The data set associated with the driver result." + dataSet: SimpleDataSet! + "The feature associated with the driver result." + feature: SimpleFeature! + "The mutation associated with the driver result." + mutation: SimpleMutation! + "A database generated id of the mutation related to the gene and mutation code associated with the driver result." + tag: SimpleTag! + "The P value of the driver result." + pValue: Float + "The fold change of the driver result." + foldChange: Float + "The log10 computed result of P value." + log10PValue: Float + "The log10 computed result of fold change." + log10FoldChange: Float + "The number or wild type genes." + numWildTypes: Int + "The number of mutant genes." + numMutants: Int +} + +type DriverResult implements BaseResult { + "A Paging object (see Paging)" + paging: Paging + "A string describing any error that may have occurred." + error: String + "A list of returned DriverResultNodes" + items: [DriverResultNode] +} diff --git a/apps/iatlas/api-gitlab/api/schema/edge.query.graphql b/apps/iatlas/api-gitlab/api/schema/edge.query.graphql new file mode 100644 index 0000000000..dc2fb8cfc7 --- /dev/null +++ b/apps/iatlas/api-gitlab/api/schema/edge.query.graphql @@ -0,0 +1,31 @@ +""" +The "Edge" type +""" +type Edge implements BaseNode { + "A unique id for the edge generated by the database. Please note that this value may change as the database gets updated and should not be relied on." + id: ID + "The label of the specific edge." + label: String + "A unique name for the Edge, usually `__`." + name: String! + "The calculated value of the Edge." + score: Float + "The starting node in the Edge." + node1: SimpleNode! + "The ending node in the Edge." + node2: SimpleNode! +} + +""" +The "EdgeResult" type + +See `Edge` +""" +type EdgeResult implements BaseResult { + "A Paging object (see Paging)" + paging: Paging + "A string describing any error that may have occurred." + error: String + "A list of returned EdgeNodes" + items: [Edge] +} diff --git a/apps/iatlas/api-gitlab/api/schema/feature.query.graphql b/apps/iatlas/api-gitlab/api/schema/feature.query.graphql new file mode 100644 index 0000000000..af2b914353 --- /dev/null +++ b/apps/iatlas/api-gitlab/api/schema/feature.query.graphql @@ -0,0 +1,116 @@ +""" +The "Feature" type may return: + +See also "SimpleFeature". +""" +type FeatureNode implements BaseNode{ + "The 'id' of the feature. Please note that this `id` is generated by the database and may not be consistent in the long term." + id: ID! + "The feature class associated with this feature." + class: String + "A readable name for the feature." + display: String + "The method tag associated with this feature." + methodTag: String + "The feature's name (a unique string for this feature)." + name: String! + "A number representing the index order the feature would be displayed in." + order: Int + "Immune traits clustered based on their Pearson correlation coefficients, described by Sayaman et al, 2021." + germlineModule: String + "Immune traits clustered based on the approach used to derive them and the parameters they intend to measure , described by Sayaman et al, 2021." + germlineCategory: String + "A list of samples that have a value for this feature" + samples: [FeatureRelatedSample!]! + "A list of samples that have a value for this feature via pseudobulk" + pseudoBulkSamples: [FeatureRelatedPseudoBulkSample!]! + "A list of cells that have a value for this feature" + cells: [FeatureRelatedCell!]! + "The type of measurement of the value." + unit: String + "The maximum value of all relationships between a specific feature and the samples." + valueMax: Float + "The minimum value of all relationships between a specific feature and the samples." + valueMin: Float +} + +type Feature implements BaseResult { + "A Paging object (see Paging)" + paging: Paging + "A string describing any error that may have occurred." + error: String + "A list of returned FeatureNodes" + items: [FeatureNode] +} + +""" +The "SimpleFeature" is a version of a `Feature`. Only basic feature attributes may be returned. + +To get more properties of features, please see "Feature" +""" +type SimpleFeature { + "A readable name for the feature." + display: String + "The feature's name (a unique string for this feature)." + name: String! + "A number representing the index order the feature would be displayed in." + order: Int + "The type of measurement of the value." + unit: String + "Immune traits clustered based on their Pearson correlation coefficients, described by Sayaman et al, 2021." + germlineModule: String + "Immune traits clustered based on the approach used to derive them and the parameters they intend to measure , described by Sayaman et al, 2021." + germlineCategory: String +} + +""" +The "SimpleFeature2" is a version of a `Feature`. Used by featureValue. + +To get more properties of features, please see "Feature" +""" +type SimpleFeature2 { + "A readable name for the feature." + display: String + "The feature's name (a unique string for this feature)." + name: String! + "A number representing the index order the feature would be displayed in." + order: Int + "The feature class associated with this feature." + class: String +} + +""" +The "FeatureRelatedSample" type is a Sample that is specifically related to a Feature. +""" +type FeatureRelatedSample { + "The sample's name." + name: String! + "The calculated relational value or the Sample related to the Feature." + value: Float +} + +""" +The "FeatureRelatedCell" is a version of a `Cell` used Feature. +""" +type FeatureRelatedCell { + "The cells name (a unique string for this cell)." + name: String! + "The type of cell" + type: String! + "The value of the cell for this feature" + value: Float! +} + +""" +The "FeatureRelatedPseudoBulkSample" type is a Sample that is specifically related to a Feature. + +See also `Feature` +""" +type FeatureRelatedPseudoBulkSample { + "The sample's name." + name: String! + "The type of cell this sample is related to" + cellType: String! + "The calculated relational value or the Sample related to the Feature." + value: Float +} \ No newline at end of file diff --git a/apps/iatlas/api-gitlab/api/schema/gene.query.graphql b/apps/iatlas/api-gitlab/api/schema/gene.query.graphql new file mode 100644 index 0000000000..288c289b3b --- /dev/null +++ b/apps/iatlas/api-gitlab/api/schema/gene.query.graphql @@ -0,0 +1,101 @@ +""" +The "Gene" type +""" +type GeneNode implements BaseNode{ + "The 'id' of the gene. Please note that this `id` is generated by the database and may not be consistent in the long term." + id: ID! + "The unique id of the gene to use on search engines." + entrez: Int! + "The HUGO Gene Nomenclature Committee." + hgnc: String! + "The 'description' of the gene." + description: String + "The 'friendlyName' of the gene." + friendlyName: String + "The IO Landscape Name of the gene." + ioLandscapeName: String + "The 'geneFamily' of the gene." + geneFamily: String + "The 'geneFunction' of the gene." + geneFunction: String + "A list of GeneTypes associated with the gene." + geneTypes: [SimpleGeneType!]! + "The 'immuneCheckpoint' of the gene." + immuneCheckpoint: String + "The 'pathway' of the gene." + pathway: String + "A list of Publications associated with the gene." + publications: [SimplePublication!]! + "A list of samples related to this gene that is associated with the value." + samples: [GeneRelatedSample!]! + "A list of samples that have a value for this gene via pseudobulk" + pseudoBulkSamples: [GeneRelatedPseudoBulkSample!]! + "A list of cells that have a value for this gene" + cells: [GeneRelatedCell!]! + "The 'superCategory' of the gene." + superCategory: String + "The 'therapyType' of the gene." + therapyType: String +} + +type Gene implements BaseResult { + "A Paging object (see Paging)" + paging: Paging + "A string describing any error that may have occurred." + error: String + "A list of returned GeneNodes" + items: [GeneNode] +} + +""" +The "SimpleGene" is a version of a gene. Only basic attributes may be returned. +""" +type SimpleGene { + "The unique id of the gene to use on search engines." + entrez: Int! + "The HUGO Gene Nomenclature Committee." + hgnc: String! + "The 'description' of the gene." + description: String + "The 'friendlyName' of the gene." + friendlyName: String + "The IO Landscape Name of the gene." + ioLandscapeName: String +} + +""" +The "SingleCellGene" is a version of a `Feature`. Used by Cells. +""" +type SingleCellGene { + "The unique id of the gene to use on search engines." + entrez: Int! + "The HUGO Gene Nomenclature Committee." + hgnc: String! + "The single cell sequencing value" + singleCellSeq: Float! +} + +""" +The "GeneRelatedPseudoBulkSample" type is a Sample that is specifically related to a Gene. + +""" +type GeneRelatedPseudoBulkSample { + "The unique name of the sample." + name: String! + "The single cell sequencing value" + singleCellSeqSum: Float! + "The sample's cell type." + cellType: String! +} + +""" +The "GeneRelatedCell" is a version of a `Cell` used by a 'Feature'. +""" +type GeneRelatedCell { + "The cells name (a unique string for this cell)." + name: String! + "The single cell sequencing value" + singleCellSeq: Float! + "The sample's cell type." + type: String! +} \ No newline at end of file diff --git a/apps/iatlas/api-gitlab/api/schema/geneType.query.graphql b/apps/iatlas/api-gitlab/api/schema/geneType.query.graphql new file mode 100644 index 0000000000..f2a92bd313 --- /dev/null +++ b/apps/iatlas/api-gitlab/api/schema/geneType.query.graphql @@ -0,0 +1,21 @@ +""" +The "GeneType" type +""" +type GeneType { + "A friendly name of the gene type." + display: String + "A list of genes related to the gene type" + genes: [SimpleGene!]! + "The proper name of the gene type." + name: String! +} + +""" +The "SimpleGeneType" is a version of a GeneType. Only basic attributes may be returned. +""" +type SimpleGeneType { + "The proper name of the gene type." + name: String! + "A friendly name of the gene type." + display: String +} diff --git a/apps/iatlas/api-gitlab/api/schema/germlineGwasResult.query.graphql b/apps/iatlas/api-gitlab/api/schema/germlineGwasResult.query.graphql new file mode 100644 index 0000000000..39d2c224ec --- /dev/null +++ b/apps/iatlas/api-gitlab/api/schema/germlineGwasResult.query.graphql @@ -0,0 +1,26 @@ +""" +The "GermlineGwasResultNode" type +""" +type GermlineGwasResultNode implements BaseNode { + "A unique id for the germline gwas result generated by the database. Please note that this value may change as the database gets updated and should not be relied on." + id: ID + "The data set associated with the germline gwas result." + dataSet: SimpleDataSet! + "The feature associated with the germline gwas result." + feature: SimpleFeature! + "The snp associated with the germline gwas result." + snp: SnpNode! + "P value for gwas result." + pValue: Float + "MAF value for gwas result." + maf: Float +} + +type GermlineGwasResult implements BaseResult { + "A Paging object (see Paging)" + paging: Paging + "A string describing any error that may have occurred." + error: String + "A list of returned GermlineGwasResultNodes" + items: [GermlineGwasResultNode!] +} diff --git a/apps/iatlas/api-gitlab/api/schema/heritabilityResult.query.graphql b/apps/iatlas/api-gitlab/api/schema/heritabilityResult.query.graphql new file mode 100644 index 0000000000..88455d08f1 --- /dev/null +++ b/apps/iatlas/api-gitlab/api/schema/heritabilityResult.query.graphql @@ -0,0 +1,30 @@ +""" +The "HeritabilityResultNode" type +""" +type HeritabilityResultNode implements BaseNode { + "A unique id for the heritability result generated by the database. Please note that this value may change as the database gets updated and should not be relied on." + id: ID + "The data set associated with the heritability result." + dataSet: SimpleDataSet! + "The feature associated with the heritability result." + feature: SimpleFeature! + "The Log-likelihood test (LRT) p-value of the heritability result." + pValue: Float + "Ancestry cluster, determined from ancestry analysis by Sayaman et al, 2021." + cluster: String + "Benjamini-Hochberg false discovery rate (FDR) adjustment for multiple testing calculated in R (p.adjust, method=BH)." + fdr: Float + "Ratio of genetic variance to phenotypic variance, estimate." + variance: Float + "Standard error." + se: Float +} + +type HeritabilityResult implements BaseResult { + "A Paging object (see Paging)" + paging: Paging + "A string describing any error that may have occurred." + error: String + "A list of returned HeritabilityResultNodes" + items: [HeritabilityResultNode] +} diff --git a/apps/iatlas/api-gitlab/api/schema/mutation.query.graphql b/apps/iatlas/api-gitlab/api/schema/mutation.query.graphql new file mode 100644 index 0000000000..079fe1b2b2 --- /dev/null +++ b/apps/iatlas/api-gitlab/api/schema/mutation.query.graphql @@ -0,0 +1,57 @@ +""" +The "StatusEnum" scalar will always be either `Mut` or `Wt`. +""" +scalar StatusEnum + +""" +The "MutationNode" type +""" +type MutationNode implements BaseNode { + "The 'id' of the mutation. Please note that this `id` is generated by the database and may not be consistent in the long term." + id: ID! + "The name of the mutation" + name: String! + "The Gene related to the mutation." + gene: SimpleGene! + "The MutationCode related to that mutation." + mutationCode: String! + "The MutationType related to that mutation." + mutationType: MutationType + "A list of Samples related to that mutation" + samples: [MutationRelatedSample!] +} + +type Mutation implements BaseResult { + "A Paging object (see Paging)" + paging: Paging + "A string describing any error that may have occurred." + error: String + "A list of returned Nodes" + items: [MutationNode] +} + +""" +The "MutationType" type +""" +type MutationType { + "A friendly name of the mutation type" + display: String + "The proper 'name' of the mutation type" + name: String! +} + +""" +The "SimpleMutation" is a version of a gene. Only basic attributes may be returned. +""" +type SimpleMutation { + "The 'id' of the mutation. Please note that this `id` is generated by the database and may not be consistent in the long term." + id: ID! + "The name of the mutation" + name: String! + "The Gene related to the mutation." + gene: SimpleGene! + "The MutationCode related to that mutation." + mutationCode: String! + "The MutationType related to that mutation." + mutationType: MutationType +} diff --git a/apps/iatlas/api-gitlab/api/schema/neoantigen.query.graphql b/apps/iatlas/api-gitlab/api/schema/neoantigen.query.graphql new file mode 100644 index 0000000000..e1b899816d --- /dev/null +++ b/apps/iatlas/api-gitlab/api/schema/neoantigen.query.graphql @@ -0,0 +1,30 @@ + +""" +The "NeoantigenNode" type +""" +type NeoantigenNode implements BaseNode { + "The 'id' of the neoantigen. Please note that this `id` is generated by the database and may not be consistent in the long term." + id: ID! + "The pmhc of the neoantigen" + pmhc: String! + "The frequency of the pmhc of the neoantigen" + freqPmhc: Int! + "The tpm of the neoantigen" + tpm: Float + "The Gene related to the neoantigen." + gene: SimpleGene + "The Patient related to the neoantigen." + patient: SimplePatient! +} + +""" +The "Neoantigen" type +""" +type Neoantigen implements BaseResult { + "A Paging object (see Paging)" + paging: Paging + "A string describing any error that may have occurred." + error: String + "A list of returned Nodes" + items: [NeoantigenNode] +} diff --git a/apps/iatlas/api-gitlab/api/schema/node.query.graphql b/apps/iatlas/api-gitlab/api/schema/node.query.graphql new file mode 100644 index 0000000000..e8e874d12b --- /dev/null +++ b/apps/iatlas/api-gitlab/api/schema/node.query.graphql @@ -0,0 +1,60 @@ +""" +The "Node" type +""" +type Node implements BaseNode { + "A unique id for the node generated by the database. Please note that this value may change as the database gets updated and should not be relied on." + id: ID + "A network identifier." + label: String + "A unique name for the node." + name: String! + "The type of network associated with the node" + network: String! + "The calculated value of the node." + score: Float + "The initial x position of the node." + x: Float + "The initial y position of the node." + y: Float + "The data set related to the node." + dataSet: SimpleDataSet! + "The gene related to the node." + gene: SimpleGene + "The feature related to the node." + feature: SimpleFeature + "The tag associated with the node" + tag1: SimpleTag + "The secondary tag associated with the node if it is stratified" + tag2: SimpleTag +} + +type NodeResult implements BaseResult { + "A Paging object (see Paging)" + paging: Paging + "A string describing any error that may have occurred." + error: String + "A list of returned Nodes" + items: [Node] +} + +""" +The "SimpleNode" is a simple version of a Node; it has no related fields. + +See also `Node` +""" +type SimpleNode { + "A unique id for the node generated by the database. Please note that this value may change as the database gets updated and should not be relied on." + id: ID + "A network identifier." + label: String + "A unique name for the node." + name: String! + "The type of network associated with the node" + network: String! + "The calculated value of the node." + score: Float + "The initial x position of the node." + x: Float + "The initial y position of the node." + y: Float +} diff --git a/apps/iatlas/api-gitlab/api/schema/paging.graphql b/apps/iatlas/api-gitlab/api/schema/paging.graphql new file mode 100644 index 0000000000..1ad8a556f4 --- /dev/null +++ b/apps/iatlas/api-gitlab/api/schema/paging.graphql @@ -0,0 +1,77 @@ +""" +The "BaseNode" interface defines an interface implemented by return types: +""" +interface BaseNode { + "A unique identifier for this record. Do not request 'id' if 'distinct' is 'true'" + id: ID +} + +""" +An object containing either cursor or offset pagination request values. +If PagingInput is omitted, paging will default to type "CURSOR", and "first" 100,000. +""" +input PagingInput { + "The type of pagination to perform. Must be either 'OFFSET' or 'CURSOR'." + type: PagingType + "When performing OFFSET paging, the page number requested." + page: Int + "When performing OFFSET paging, the number or records requested." + limit: Int + "When performing CURSOR paging: the CURSOR to be used in tandem with 'first'" + first: Int + "When performing CURSOR paging: the CURSOR to be used in tandem with 'last'" + last: Int + "When performing CURSOR paging, the number of records requested BEFORE the CURSOR." + before: String + "When performing CURSOR paging, the number of records requested AFTER the CURSOR." + after: String +} + +""" +An ENUM containing constants for the two types of pagination available. +'CURSOR' is default +""" +enum PagingType { + "'CURSOR': more performant pagination. Cannot seek to specific pages. May not be used in conjunction with 'distinct'" + CURSOR + "OFFSET: may be significantly less performant, especially when used in conjunction with 'distinct', but necessary when paged 'distinct' result sets are required." + OFFSET +} + +""" +Pagination metadata returned with each paginated request. +""" +type Paging { + "Must be set to 'OFFSET' or 'CURSOR'. When not passed, will default to 'CURSOR'. See (PagingType)." + type: PagingType + "The total number of pages available based on the requested limit/first/last value." + pages: Int + "The total number of records available matching the given request." + total: Int + "When performing OFFSET paging, the page number returned." + page: Int + "When performing OFFSET paging, the number of requested records per page." + limit: Int + "When performing CURSOR paging, a Boolean indicating the presence or absence of additional pages __after__ the __endCursor__." + hasNextPage: Boolean + "When performing CURSOR paging, a Boolean indicating the presence or absence of additional pages __before__ the __startCursor__." + hasPreviousPage: Boolean + "When performing CURSOR paging, the cursor of the first record returned." + startCursor: String + "When performing CURSOR paging, the cursor of the last record returned." + endCursor: String + "The number of items returned in this response. May be less than the number requested." + returned: Int +} + +""" +Provides a common interface for return data. +""" +interface BaseResult { + "Pagination metadata. (see Paging)" + paging: Paging + "A string error message returned by the API and may be displayed to the user by the client. This is separate from 'errors' which provides useful feedback during client development." + error: String + "A list of returned objects that implement the BaseNode interface." + items: [BaseNode] +} diff --git a/apps/iatlas/api-gitlab/api/schema/patient.query.graphql b/apps/iatlas/api-gitlab/api/schema/patient.query.graphql new file mode 100644 index 0000000000..d6aa548e52 --- /dev/null +++ b/apps/iatlas/api-gitlab/api/schema/patient.query.graphql @@ -0,0 +1,69 @@ +""" +The "EthnicityEnum" scalar will always be either `not hispanic or latino` or `hispanic or latino`. +""" +scalar EthnicityEnum + +""" +The "GenderEnum" scalar will always be either `female` or `male`. +""" +scalar GenderEnum + +""" +The "RaceEnum" scalar will always be either `american indian or alaska native`, `black or african american`, `native hawaiian or other pacific islander`, `white`, or `asian`. +""" +scalar RaceEnum + +""" +The "Patient" type +""" +type PatientNode implements BaseNode { + "A unique id for the driver result generated by the database. Please note that this value may change as the database gets updated and should not be relied on." + id: ID! + "The age of the patient at the time of diagnosis." + ageAtDiagnosis: Int + "The barcode of the patient." + barcode: String! + "The ethnicity of the patient." + ethnicity: EthnicityEnum + "The gender of the patient." + gender: GenderEnum + "The height of the patient." + height: Float + "The race of the patient." + race: RaceEnum + "The weight of the patient." + weight: Float + "A list of samples associated with the patient." + samples: [String!]! + "A list of slides associated with the patient." + slides: [SimpleSlide!]! +} + +type Patient implements BaseResult { + "A Paging object (see Paging)" + paging: Paging + "A string describing any error that may have occurred." + error: String + "A list of returned PatientNode" + items: [PatientNode] +} + +""" +The "SimplePatient" type +""" +type SimplePatient { + "The age of the patient at the time of diagnosis." + ageAtDiagnosis: Int + "The barcode of the patient." + barcode: String! + "The ethnicity of the patient." + ethnicity: EthnicityEnum + "The gender of the patient." + gender: GenderEnum + "The height of the patient." + height: Float + "The race of the patient." + race: RaceEnum + "The weight of the patient." + weight: Float +} diff --git a/apps/iatlas/api-gitlab/api/schema/publication.query.graphql b/apps/iatlas/api-gitlab/api/schema/publication.query.graphql new file mode 100644 index 0000000000..149e3a47ab --- /dev/null +++ b/apps/iatlas/api-gitlab/api/schema/publication.query.graphql @@ -0,0 +1,45 @@ +""" +The "Publication" type +""" +type Publication { + "The first last name of the publication's author" + firstAuthorLastName: String + "The id of the publication in the doi website. See: https://www.doi.org/" + doId: String + "A list of Genes related to the publication" + genes: [SimpleGene!] + "A list of GeneTypes related to the publication" + geneTypes: [SimpleGeneType!] + "The 'journal' of the publication." + journal: String + "The unique name of the publication. This is generated as a combination of the do id and the pubmed id." + name: String! + "The id of the publication in the pubmed website. See https://pubmed.ncbi.nlm.nih.gov/" + pubmedId: Int + "A list of Tags related to the publication" + tags: [SimpleTag!] + "The 'title' of the publication." + title: String + "The 'year' of the publication." + year: Int +} + +""" +The "SimplePublication" type is a version of a Publication. Only basic attributes may be returned. +""" +type SimplePublication { + "The first last name of the publication's author" + firstAuthorLastName: String + "The id of the publication in the doi website. See: https://www.doi.org/" + doId: String + "The 'journal' of the publication." + journal: String + "The unique name of the publication. This is generated as a combination of the do id and the pubmed id." + name: String! + "The id of the publication in the pubmed website. See https://pubmed.ncbi.nlm.nih.gov/" + pubmedId: Int + "The 'title' of the publication." + title: String + "The 'year' of the publication." + year: Int +} diff --git a/apps/iatlas/api-gitlab/api/schema/rareVariantPathwayAssociation.query.graphql b/apps/iatlas/api-gitlab/api/schema/rareVariantPathwayAssociation.query.graphql new file mode 100644 index 0000000000..b61ef4600d --- /dev/null +++ b/apps/iatlas/api-gitlab/api/schema/rareVariantPathwayAssociation.query.graphql @@ -0,0 +1,40 @@ +""" +The "RareVariantPathwayAssociationNode" type +""" +type RareVariantPathwayAssociationNode implements BaseNode { + "A unique id for the rare variant pathway association generated by the database. Please note that this value may change as the database gets updated and should not be relied on." + id: ID + "The data set associated with the rare variant pathway association." + dataSet: SimpleDataSet! + "The feature associated with the rare variant pathway association." + feature: SimpleFeature! + "The biological or functional category." + pathway: String + "The P value of the rare variant pathway association." + pValue: Float + "Minimum value for the rare variant pathway association" + min: Float + "Maximum value for the rare variant pathway association" + max: Float + "Mean value for the rare variant pathway association" + mean: Float + "Quartile one value for the rare variant pathway association" + q1: Float + "Quartile two value for the rare variant pathway association" + q2: Float + "Quartile three value for the rare variant pathway association" + q3: Float + "Number of samples that were analyzed with a mutation in at least one gene in the pathway." + nMutants: Int + "Number of samples that were analyzed." + nTotal: Int +} + +type RareVariantPathwayAssociation implements BaseResult { + "A Paging object (see Paging)" + paging: Paging + "A string describing any error that may have occurred." + error: String + "A list of returned RareVariantPathwayAssociationNodes" + items: [RareVariantPathwayAssociationNode] +} diff --git a/apps/iatlas/api-gitlab/api/schema/root.query.graphql b/apps/iatlas/api-gitlab/api/schema/root.query.graphql new file mode 100644 index 0000000000..8cb62ab1ac --- /dev/null +++ b/apps/iatlas/api-gitlab/api/schema/root.query.graphql @@ -0,0 +1,609 @@ +type Query { + + """ + If no arguments are passed, this will return all cells + """ + cells( + "An instance of PagingInput (see PagingInput)" + paging: PagingInput + "A boolean specifying whether or not duplicates should be filtered out. Default is false. Set to 'true' only when necessary, as it negatively impacts performance." + distinct: Boolean + "A unique id for the result generated by the database. PLEASE NOTE: this ID should not be relied on, it may change as the database changes." + id: ID + "A list of cohort names associated with the cell to filter by." + cohort: [String!] + "A a list of cells names to filter by" + cell: [String!] + ): Cell! + + """ + The data structure containing cell stats + + If no arguments are passed, this will return all cell stats + """ + cellStats( + "An instance of PagingInput (see PagingInput)" + paging: PagingInput + "A boolean specifying whether or not duplicates should be filtered out. Default is false. Set to 'true' only when necessary, as it negatively impacts performance." + distinct: Boolean + "A unique id for the result generated by the database. PLEASE NOTE: this ID should not be relied on, it may change as the database changes." + id: ID + "A list of entrez ids to look up" + entrez: [Int!] + ): CellStat! + + """ + The data structure containing Cohorts + + If no arguments are passed, this will return all cohorts + """ + cohorts( + "An instance of PagingInput (see PagingInput)" + paging: PagingInput + "A boolean specifying whether or not duplicates should be filtered out. Default is false. Set to 'true' only when necessary, as it negatively impacts performance." + distinct: Boolean + "A unique id for the result generated by the database. PLEASE NOTE: this ID should not be relied on, it may change as the database changes." + id: ID + "A list of cohort names associated with the cohort to filter by." + cohort: [String!] + "A list of data set names associated with the cohort to filter by." + dataSet: [String!] + "A list of tag names associated with the cohort to filter by." + tag: [String!] + ): Cohort! + """ + The data structure containing Colocalizations. + + If no arguments are passed, this will return all colocalization. + """ + colocalizations( + "An instance of PagingInput (see PagingInput)" + paging: PagingInput + "A boolean specifying whether or not duplicates should be filtered out. Default is false. Set to 'true' only when necessary, as it negatively impacts performance." + distinct: Boolean + "A unique id for the result generated by the database. PLEASE NOTE: this ID should not be relied on, it may change as the database changes." + id: ID + "A list of data set names associated with the colocalization to filter by." + dataSet: [String!] + "A list of data set names whose genes were used to compute the colocalization." + colocDataSet: [String!] + "A list of feature names associated with the colocalization to filter by." + feature: [String!] + "A list of gene entrez ids associated with the colocalization to filter by." + entrez: [Int!] + "A list of snp names associated with the colocalization to filter by." + snp: [String!] + "The QTL type of the colocalization to filter by. (either 'sQTL' or 'eQTL')." + qtlType: QTLTypeEnum + "The eCaviar colocalization posterior probability locus model. (either 'C1' or 'C2')." + eCaviarPP: ECaviarPPEnum + "Type of colocalization plot. (either '3 Level Plot' or 'Expanded Region')." + plotType: ColocPlotTypeEnum + ): Colocalization! + + """ + The data structure containing Copy Number Results. + + If no arguments are passed, this will return all copy number results. + """ + copyNumberResults( + "An instance of PagingInput (see PagingInput)" + paging: PagingInput + "A boolean specifying whether or not duplicates should be filtered out. Default is false. Set to 'true' only when necessary, as it negatively impacts performance." + distinct: Boolean + "A unique id for the result generated by the database. PLEASE NOTE: this ID should not be relied on, it may change as the database changes." + id: ID + "A list of data set names associated with the copy number results to filter by." + dataSet: [String!] + "A list of 'related' tag names associated with the data set that is associated with the results to filter by." + related: [String!] + "A list of feature names associated with the copy number results to filter by." + feature: [String!] + "A list of gene entrez ids associated with the copy number results to filter by." + entrez: [Int!] + "A list of tag names associated with the copy number results to filter by." + tag: [String!] + "The direction of the copy number results to filter by. (either 'Amp' or 'Del')" + direction: DirectionEnum + "A minimum P value to filter the copy number results by." + minPValue: Float + "A maximum P value to filter the copy number results by." + maxPValue: Float + "A minimum log10 calculation of the P value to filter the copy number results by." + minLog10PValue: Float + "A maximum log10 calculation of the P value to filter the copy number results by." + maxLog10PValue: Float + "A minimum mean normal to filter the copy number results by." + minMeanNormal: Float + "A minimum mean CNV to filter the copy number results by." + minMeanCnv: Float + "A minimum T stat to filter the copy number results by." + minTStat: Float + ): CopyNumberResult! + + """ + The data structure containing Data Sets. + + If no arguments are passed, this will return all data sets. + """ + dataSets( + "An instance of PagingInput (see PagingInput)" + paging: PagingInput + "A boolean specifying whether or not duplicates should be filtered out. Default is false. Set to 'true' only when necessary, as it negatively impacts performance." + distinct: Boolean + "A unique id for the result generated by the database. PLEASE NOTE: this ID should not be relied on, it may change as the database changes." + id: ID + "A list of data set names to look up." + dataSet: [String!] + "A list of sample names associated with the data sets to filter by." + sample: [String!] + "A list of data set types to filter by." + dataSetType: [String!] + ): DataSet! + + """ + The "driverResults" query + + If no arguments are passed, this will return all driver results. + """ + driverResults( + "An instance of PagingInput (see PagingInput)" + paging: PagingInput + "A boolean specifying whether or not duplicates should be filtered out. Default is false. Set to 'true' only when necessary, as it negatively impacts performance." + distinct: Boolean + "A unique id for the result generated by the database. PLEASE NOTE: this ID should not be relied on, it may change as the database changes." + id: ID + "A list of data set names associated with the driver results to filter by" + dataSet: [String!] + "A list of 'related' tag names associated with the data set that is associated with the results to filter by." + entrez: [Int!] + "A list of feature names associated with the driver results to filter by." + feature: [String!] + "A list of mutation names associated with the driver results to filter by." + mutation: [String!] + "A list of mutation code names associated with the mutation of with the driver results to filter by." + mutationCode: [String!] + "A list of tag names associated with the driver results to filter by." + related: [String!] + "A list of gene entrez ids associated with the gene of the mutation of the driver results to filter by." + tag: [String!] + "A list of mutation code names associated with the mutation of with the driver results to filter by." + maxPValue: Float + "A minimum P value to filter the driver results by." + minPValue: Float + "A maximum log10 calculation of the P value to filter the driver results by." + maxLog10PValue: Float + "A minimum log10 calculation of the P value to filter the driver results by." + minLog10PValue: Float + "A minimum fold change to filter the driver results by." + minFoldChange: Float + "A minimum log10 calculation of the fold change to filter the driver results by." + minLog10FoldChange: Float + "A minimum of wild type genes to filter the driver results by." + minNumWildTypes: Int + "A minimum mutant genes to filter the driver results by." + minNumMutants: Int + ): DriverResult! + + """ + The "edges" query + + If no arguments are passed, this will return all edges (please note, there will be a LOT of results). + """ + edges( + "An instance of PagingInput (see PagingInput)" + paging: PagingInput + "A boolean specifying whether or not duplicates should be filtered out. Default is false. Set to 'true' only when necessary, as it negatively impacts performance." + distinct: Boolean + "The maximum score value to return." + maxScore: Float + "The minimum score value to return." + minScore: Float + "A list of starting node names." + node1: [String!] + "A list of ending node names." + node2: [String!] + ): EdgeResult! + + """ + The "features" query + + If no arguments are passed, this will return all features. + + """ + features( + "An instance of PagingInput (see PagingInput)" + paging: PagingInput + "A boolean specifying whether or not duplicates should be filtered out. Default is false. Set to 'true' only when necessary, as it negatively impacts performance." + distinct: Boolean + "A unique id for the gene generated by the database. PLEASE NOTE: this ID should not be relied on, it may change as the database changes." + id: ID + "A list of feature names associated with the features to filter by." + feature: [String!] + "A list of feature class names associated with the features to filter by." + featureClass: [String!] + "A list of sample names associated with the feature to filter by." + sample: [String!] + "A list of cohort names associated with the feature to filter by." + cohort: [String!] + "The maximum value (relationship between the feature and the sample) to filter by." + maxValue: Float + "The minimum value (relationship between the feature and the sample) to filter by." + minValue: Float + ): Feature! + + """ + The "genes" query + + If no arguments are passed, this will return all genes. + """ + genes( + "An instance of PagingInput (see PagingInput)" + paging: PagingInput + "A boolean specifying whether or not duplicates should be filtered out. Default is false. Set to 'true' only when necessary, as it negatively impacts performance." + distinct: Boolean + "A unique id for the gene generated by the database. PLEASE NOTE: this ID should not be relied on, it may change as the database changes." + id: ID + "A list of data set names related to samples that are related to the genes to look up." + entrez: [Int!] + "A list of gene types related to the genes to look up." + geneType: [String!] + "A list of cohort names associated with the feature to filter by." + cohort: [String!] + "A list of sample names related to the feature to filter by." + sample: [String!] + "The maximum RNA Sequence Expression value related to the genes to look up." + maxRnaSeqExpr: Float + "The minimum RNA Sequence Expression value related to the genes to look up." + minRnaSeqExpr: Float + + ): Gene! + + """ + The "geneTypes" query + """ + geneTypes( + "A list of names of the gene types to look up." + name: [String!] + ): [GeneType!]! + + """ + The "germlineGwasResults" query + + If no arguments are passed, this will return all germline gwas results. + """ + germlineGwasResults( + "An instance of PagingInput (see PagingInput)" + paging: PagingInput + "A boolean specifying whether or not duplicates should be filtered out. Default is false. Set to 'true' only when necessary, as it negatively impacts performance." + distinct: Boolean + "A list of data set names associated with the germline gwas results to filter by" + dataSet: [String!] + "A list of feature names associated with the germline gwas results to filter by." + feature: [String!] + "A list of snp names associated with the germline gwas results to filter by." + snp: [String!] + "A maximum P value to filter the germline gwas results by." + maxPValue: Float + "A minimum P value to filter the germline gwas results by." + minPValue: Float + ): GermlineGwasResult! + + + """ + The "heritabilityResults" query + + If no arguments are passed, this will return all heritability results. + """ + heritabilityResults( + "An instance of PagingInput (see PagingInput)" + paging: PagingInput + "A boolean specifying whether or not duplicates should be filtered out. Default is false. Set to 'true' only when necessary, as it negatively impacts performance." + distinct: Boolean + "A list of data set names associated with the heritability results to filter by" + dataSet: [String!] + "A list of feature names associated with the heritability results to filter by." + feature: [String!] + "A maximum P value to filter the heritability results by." + maxPValue: Float + "A minimum P value to filter the heritability results by." + minPValue: Float + "Immune traits clustered based on their Pearson correlation coefficients, described by Sayaman et al, 2021." + cluster: [String!] + ): HeritabilityResult! + + """ + The "mutations" type + + If no arguments are passed, this will return all mutations. + """ + mutations( + "An instance of PagingInput (see PagingInput)" + paging: PagingInput + "A boolean specifying whether or not duplicates should be filtered out. Default is false. Set to 'true' only when necessary, as it negatively impacts performance." + distinct: Boolean + "A unique id for the gene generated by the database. PLEASE NOTE: this ID should not be relied on, it may change as the database changes." + id: ID + "A list of names associated with the mutations to filter by." + mutation: [String!] + "A list of gene entrez ids associated with the mutations to filter by." + entrez: [Int!] + "A list of mutation code names associated with the mutations to filter by." + mutationCode: [String!] + "A list of mutation type names associated with the mutations to filter by." + mutationType: [String!] + "A list of cohort names associated with the mutations to filter by." + cohort: [String!] + "A list of sample names associated with the mutations to filter by." + sample: [String!] + "A list of statuses associated relationship between mutation and sample to filter by." + status: [StatusEnum!] + ): Mutation! + + """ + The "mutationTypes" query returns all mutation types. + """ + mutationTypes: [MutationType!]! + + """ + The "neoantigens" type + + If no arguments are passed, this will return all neoantigens. + """ + neoantigens( + "An instance of PagingInput (see PagingInput)" + paging: PagingInput + "A boolean specifying whether or not duplicates should be filtered out. Default is false. Set to 'true' only when necessary, as it negatively impacts performance." + distinct: Boolean + "A unique id for the neoantigen. PLEASE NOTE: this ID should not be relied on, it may change as the database changes." + id: ID + "A list of phmhcs to filter by." + pmhc: [String!] + "A list of gene entrez ids associated with the neoantigens to filter by." + entrez: [Int!] + "A list of patient names associated with the neoantigens to filter by." + patient: [String!] + ): Neoantigen! + + """ + The "nodes" query + + If no arguments are passed, this will return all nodes (please note, there will be a LOT of results). + """ + nodes( + "An instance of PagingInput (see PagingInput)" + paging: PagingInput + "A boolean specifying whether or not duplicates should be filtered out. Default is false. Set to 'true' only when necessary, as it negatively impacts performance." + distinct: Boolean + "A list of data set names associated with the nodes to filter by." + dataSet: [String!] + "A list of gene entrez ids associated with the nodes to filter by." + entrez: [Int!] + "A list of feature names associated with the nodes to filter by." + feature: [String!] + "A list of feature class names associated with features related to the nodes to filter by." + featureClass: [String!] + "A list of gene type names associated with genes related to the nodes to filter by." + geneType: [String!] + "The maximum score associated with the nodes to filter by." + maxScore: Float + "The minimum score associated with the nodes to filter by" + minScore: Float + "A list of tag names associated with the nodes that are also associated with the 'network' tag to filter by" + network: [String!] + "A list of tag names related to the data set associated with the nodes to filter by" + related: [String!] + "A list of tag names associated with the nodes to filter by" + tag1: [String!] + "A list of secondary tag names associated with the nodes to filter by" + tag2: [String!] + "The number of tags the node should have, either 1 or 2" + nTags: Int + ): NodeResult + + """ + The "patients" query + + If no arguments are passed, this will return all patients. + """ + patients( + "An instance of PagingInput (see PagingInput)" + paging: PagingInput + "A boolean specifying whether or not duplicates should be filtered out. Default is false. Set to 'true' only when necessary, as it negatively impacts performance." + distinct: Boolean + "A unique id for the result generated by the database. PLEASE NOTE: this ID should not be relied on, it may change as the database changes." + id: ID + "A list of patient barcodes of the patients to look up." + barcode: [String!] + "A list of data set names associated with the samples related to the patient to filter by" + dataSet: [String!] + "A list of patient ethnicities to filter the patients by" + ethnicity: [EthnicityEnum!] + "A list of patient genders to filter the patients by" + gender: [GenderEnum!] + "A number representing the maximum age of the patient at the time of diagnosis to filter the patients by" + maxAgeAtDiagnosis: Int + "A number representing the maximum patient height to filter the patients by" + maxHeight: Float + "A number representing the maximum patient weight to filter the patients by" + maxWeight: Float + "A number representing the minimum age of the patient at the time of diagnosis to filter the patients by" + minAgeAtDiagnosis: Int + "A number representing the minimum patient height to filter the patients by" + minHeight: Float + "A number representing the minimum patient weight to filter the patients by" + minWeight: Float + "A list of patient races to filter the patients by" + race: [RaceEnum!] + "A list of sample names to filter the patients by" + sample: [String!] + "A list of slide names to filter the patients by" + slide: [String!] + ): Patient! + + """ + The "rareVariantPathwayAssociation" query + + If no arguments are passed, this will return all rare variant pathway associations. + """ + rareVariantPathwayAssociations( + "An instance of PagingInput (see PagingInput)" + paging: PagingInput + "A boolean specifying whether or not duplicates should be filtered out. Default is false. Set to 'true' only when necessary, as it negatively impacts performance." + distinct: Boolean + "A unique id for the rare variant pathway association generated by the database. PLEASE NOTE: this ID should not be relied on, it may change as the database changes." + id: ID + "A list of data set names associated with the rare variant pathway associations to filter by" + dataSet: [String!] + "A list of feature names associated with the rare variant pathway associations to filter by." + feature: [String!] + "A list of pathways associated with the rare variant pathway associations to filter by." + pathway: [String!] + "A maximum P value to filter the rare variant pathway associations by." + maxPValue: Float + "A minimum P value to filter the rare variant pathway associations by." + minPValue: Float + ): RareVariantPathwayAssociation! + + """ + The "samples" query + + If no filters are passed, this will return all samples. + """ + samples( + "An instance of PagingInput (see PagingInput)" + paging: PagingInput + "A boolean specifying whether or not duplicates should be filtered out. Default is false. Set to 'true' only when necessary, as it negatively impacts performance." + distinct: Boolean + "A unique id for the result generated by the database. PLEASE NOTE: this ID should not be relied on, it may change as the database changes." + id: ID + "A list of patient ethnicities to filter the samples by" + ethnicity: [EthnicityEnum!] + "A list of patient genders to filter the samples by" + gender: [GenderEnum!] + "A number representing the maximum age of the patient at the time of diagnosis to filter the samples by" + maxAgeAtDiagnosis: Int + "A number representing the maximum patient height to filter the samples by" + maxHeight: Float + "A number representing the maximum patient weight to filter the samples by" + maxWeight: Float + "A number representing the minimum age of the patient at the time of diagnosis to filter the samples by" + minAgeAtDiagnosis: Int + "A number representing the minimum patient height to filter the samples by" + minHeight: Float + "A number representing the minimum patient weight to filter the samples by" + minWeight: Float + "A list of sample names to filter the samples by" + name: [String!] + "A list of patient barcodes to filter the samples by" + patient: [String!] + "A list of patient races to filter the samples by" + race: [RaceEnum!] + ): Sample! + + + """ + The "slides" query + + If no arguments are passed, this will return all slides. + """ + slides( + "An instance of PagingInput (see PagingInput)" + paging: PagingInput + "A boolean specifying whether or not duplicates should be filtered out. Default is false. Set to 'true' only when necessary, as it negatively impacts performance." + distinct: Boolean + "A unique id for the result generated by the database. PLEASE NOTE: this ID should not be relied on, it may change as the database changes." + id: ID + "A list of patient barcodes to filter the slides by." + barcode: [String!] + "A list of patient ethnicities to filter the slides by." + ethnicity: [EthnicityEnum!] + "A list of patient genders to filter the slides by." + gender: [GenderEnum!] + "A number representing the maximum age of the patient at the time of diagnosis to filter the slides by." + maxAgeAtDiagnosis: Int + "A number representing the maximum patient height to filter the slides by." + maxHeight: Float + "A number representing the maximum patient weight to filter the slides by." + maxWeight: Float + "A number representing the minimum age of the patient at the time of diagnosis to filter the slides by." + minAgeAtDiagnosis: Int + "A number representing the minimum patient height to filter the slides by." + minHeight: Float + "A number representing the minimum patient weight to filter the slides by." + minWeight: Float + "A list of slide names to look up." + name: [String!] + "A list of patient races to filter the slides by." + race: [RaceEnum!] + "A list of samples related to the slides to filter by." + sample: [String!] + ): Slide! + + """ + The "snps" query + + If no arguments are passed, this will return all snps. + """ + snps( + "An instance of PagingInput (see PagingInput)" + paging: PagingInput + "A boolean specifying whether or not duplicates should be filtered out. Default is false. Set to 'true' only when necessary, as it negatively impacts performance." + distinct: Boolean + "A list of snp names to filter by." + name: [String!] + "A list of snp rsids to filter by." + rsid: [String!] + "A list of chromosomes to filter by." + chr: [String!] + "A maximum basepair value to filter snps by." + maxBP: Int + "A minimum basepair value to filter snps by." + minBP: Int + ): Snp! + + """ + The "tags" query + """ + tags( + "An instance of PagingInput (see PagingInput)" + paging: PagingInput + "A boolean specifying whether or not duplicates should be filtered out. Default is false. Set to 'true' only when necessary, as it negatively impacts performance." + distinct: Boolean + "A list of data set names." + dataSet: [String!] + "A list of tag names related to the data set(s)." + related: [String!] + "A list of tag names." + tag: [String!] + "A list of sample names." + sample: [String!] + "A list of cohort names" + cohort: [String!] + "A list of tag types: 'group' or 'parent_group' or 'meta_group' or 'network'" + type: [TagTypeEnum!] + ): Tag! + + """ + A simple test query that is independent of the database. + """ + test: TestPage! +} + +type TestPage { + items: TestFields! + page: Int! +} + +type TestFields { + contentType: String! + userAgent: String! + headers: TestHeaders! +} + +type TestHeaders { + contentLength: Int! + contentType: String! + host: String! + userAgent: String! +} diff --git a/apps/iatlas/api-gitlab/api/schema/sample.query.graphql b/apps/iatlas/api-gitlab/api/schema/sample.query.graphql new file mode 100644 index 0000000000..0a14d97ae5 --- /dev/null +++ b/apps/iatlas/api-gitlab/api/schema/sample.query.graphql @@ -0,0 +1,107 @@ +""" +The "SampleNode" type +""" +type SampleNode implements BaseNode { + "A unique id for the driver result generated by the database. Please note that this value may change as the database gets updated and should not be relied on." + id: ID! + "The sample's name (often the 'sample' portion of a [TCGA barcode](https://docs.gdc.cancer.gov/Encyclopedia/pages/TCGA_Barcode/))." + name: String! + "A Patient structure related to the sample. There may only ever be one patient to a sample though a patient may be related to many samples." + patient: SimplePatient +} + +type Sample implements BaseResult { + "A Paging object (see Paging)" + paging: Paging + "A string describing any error that may have occurred." + error: String + "A list of returned SampleNodes" + items: [SampleNode] +} + +""" +The "GeneRelatedSample" type is a Sample that is specifically related to a Gene. +""" +type GeneRelatedSample { + "The sample's name (often the 'sample' portion of a [TCGA barcode](https://docs.gdc.cancer.gov/Encyclopedia/pages/TCGA_Barcode/))." + name: String! + "The the RNASeq expression value of the Sample related to the Gene." + rnaSeqExpr: Float + "The the nanostring expression value of the Sample related to the Gene." + nanostringExpr: Float +} + +""" +The "CellTypeFeatureRelatedSample" type is a Sample that is specifically related to a Feature and a cell type + +See also `Feature` +""" +type CellTypeFeatureRelatedSample { + "The sample's name." + name: String! + "The type of cell this sample is related to" + cellType: String! + "The calculated relational value or the Sample related to the Feature." + value: Float +} + + +""" +The "CellTypeGeneRelatedSample" type is a Sample that is specifically related to a Gene and a cell type + +See also `Feature` +""" +type CellTypeGeneRelatedSample { + "The sample's name." + name: String! + "The type of cell this sample is related to" + cellType: String! + "The the pseudobulk sum single seq value of the Sample related to the Gene." + singleCellSeqSum: Float +} + +""" +The "MutationRelatedSample" type is a Sample that is specifically related to a Mutation. + +See also `Sample`, `Mutation`, `StatusEnum`, and `SimplePatient` +""" +type MutationRelatedSample { + "The sample's name (often the 'sample' portion of a [TCGA barcode](https://docs.gdc.cancer.gov/Encyclopedia/pages/TCGA_Barcode/))." + name: String! + "The Patient related to the Sample" + patient: SimplePatient + "The status of the Sample, either `Mut` or `Wt`, related to the Mutation." + status: StatusEnum! +} + +""" +The "SampleByMutationStatus" type +""" +type SampleByMutationStatus { + "The 'status' the Mutation, either `Mut` or `Wt`." + status: StatusEnum + "A list of Samples associated with the mutation status." + samples: [Sample!]! +} + +""" +The "SimpleSample" is a simple version of a Sample; it has no related fields. + +See also `Sample` +""" +type SimpleSample { + "The sample's name (often the 'sample' portion of a [TCGA barcode](https://docs.gdc.cancer.gov/Encyclopedia/pages/TCGA_Barcode/))." + name: String! +} + +""" +The "CohortSample" is a simple version of a Sample; it has no related fields. + +See also `Sample` +""" +type CohortSample { + "The sample's name (often the 'sample' portion of a [TCGA barcode](https://docs.gdc.cancer.gov/Encyclopedia/pages/TCGA_Barcode/))." + name: String! + "The Tag for the samples cohort." + tag: SimpleTag +} diff --git a/apps/iatlas/api-gitlab/api/schema/slide.query.graphql b/apps/iatlas/api-gitlab/api/schema/slide.query.graphql new file mode 100644 index 0000000000..1fb4ed683f --- /dev/null +++ b/apps/iatlas/api-gitlab/api/schema/slide.query.graphql @@ -0,0 +1,32 @@ +""" +The "Slide" type +""" +type SlideNode implements BaseNode { + "A unique id for the slide by the database. Please note that this value may change as the database gets updated and should not be relied on." + id: ID! + "The name of the slide." + name: String! + "The description of the slide." + description: String + "The patient associated to the slide." + patient: SimplePatient +} + +type Slide implements BaseResult { + "A Paging object (see Paging)" + paging: Paging + "A string describing any error that may have occurred." + error: String + "A list of returned SlideNodes" + items: [SlideNode] +} + +""" +The "SimpleSlide" type +""" +type SimpleSlide { + "The name of the slide." + name: String! + "The description of the slide." + description: String +} diff --git a/apps/iatlas/api-gitlab/api/schema/snp.query.graphql b/apps/iatlas/api-gitlab/api/schema/snp.query.graphql new file mode 100644 index 0000000000..60d5ffe48b --- /dev/null +++ b/apps/iatlas/api-gitlab/api/schema/snp.query.graphql @@ -0,0 +1,26 @@ +""" +The "SnpNode" type may return: + +""" +type SnpNode implements BaseNode { + "A unique id for the snp generated by the database. Please note that this value may change as the database gets updated and should not be relied on." + id: ID + "The snp's name (a unique string for this snp)." + name: String! + "The rsid of the snp." + rsid: String + "The chromosome of the snp." + chr: String + "The basepair location of the snp" + bp: Int +} + + +type Snp implements BaseResult { + "A Paging object (see Paging)" + paging: Paging + "A string describing any error that may have occurred." + error: String + "A list of returned Snps" + items: [SnpNode] +} \ No newline at end of file diff --git a/apps/iatlas/api-gitlab/api/schema/tag.query.graphql b/apps/iatlas/api-gitlab/api/schema/tag.query.graphql new file mode 100644 index 0000000000..386c300098 --- /dev/null +++ b/apps/iatlas/api-gitlab/api/schema/tag.query.graphql @@ -0,0 +1,78 @@ +""" +'group' or 'parent_group' or 'meta_group' or 'network' +""" +scalar TagTypeEnum + + +""" +The "Tag" type + +See also `SimpleTag` +""" +type TagNode implements BaseNode { + "A unique id for the data set generated by the database. Please note that this value may change as the database gets updated and should not be relied on." + id: ID + "The 'characteristics' or description of the tag." + characteristics: String + "A color to represent the tag as a hex value." + color: String + "A long display name for the tag used in text descriptions." + longDisplay: String! + "The name of the tag." + name: String! + "A number representing the index order the tag would be displayed in." + order: Int + "A list of Publications related to the tag." + publications: [SimplePublication!] + "A tag related to the tag." + related: [SimpleTag!] + "The number of sample associated with the tag." + sampleCount: Int! + "A list of the samples associated with the tag." + samples: [SimpleSample!] + "A friendly name for the tag (used in plots)." + shortDisplay: String! + "The type of tag." + type: TagTypeEnum! +} + +type Tag implements BaseResult { + "A Paging object (see Paging)" + paging: Paging + "A string describing any error that may have occurred." + error: String + "A list of returned TagNodes" + items: [TagNode] +} + +""" +A "SimpleTag" type is a version of a `Tag`. Only basic tag attributes may be returned. +""" +type SimpleTag { + "The 'characteristics' or description of the tag." + characteristics: String + "A color to represent the tag as a hex value." + color: String + "A long display name for the tag used in text descriptions." + longDisplay: String + "The name of the tag." + name: String! + "A number representing the index order the tag would be displayed in." + order: Int + "A friendly name for the tag (used in plots)." + shortDisplay: String + "The type of tag." + type: TagTypeEnum! +} + +""" +The "RelatedByDataSet" type +""" +type RelatedByDataSet { + "The name of the DataSet" + dataSet: String! + "A friendly display name for the data set." + display: String + "A list of SimpleTags related to the DataSet" + related: [SimpleTag!]! +} diff --git a/apps/iatlas/api-gitlab/api/telemetry/PROFILING.md b/apps/iatlas/api-gitlab/api/telemetry/PROFILING.md new file mode 100644 index 0000000000..a2b7fc0c04 --- /dev/null +++ b/apps/iatlas/api-gitlab/api/telemetry/PROFILING.md @@ -0,0 +1,14 @@ +# iAtlas API Profiling + +[BACK TO MAIN README](./../../README.md) + +## Deterministic Profiling + +Functions may be profiled using the `@profile(__name__)` decorator. Adding this decorator to a function will cause the app to write a profile for that function when it is called. The profile may be reviewed via [SnakeViz](https://jiffyclub.github.io/snakeviz/). Simply call: + +```sh +./view_profile.sh +``` + +from the root of the project. A list of profile options will be given. Once a profile is selected, the SnakeViz server will render the profile as a webpage. The webpage URL will be displayed in the console. Go to the page in your browser to view the profile. +By default, SnakeViz runs on port `8020`. To change this port, set the SNAKEVIZ_PORT variable in a `.env-dev` file in the root of the project (see the `.env-SAMPLE` for an example.) diff --git a/apps/iatlas/api-gitlab/api/telemetry/__init__.py b/apps/iatlas/api-gitlab/api/telemetry/__init__.py new file mode 100644 index 0000000000..c0c090a73e --- /dev/null +++ b/apps/iatlas/api-gitlab/api/telemetry/__init__.py @@ -0,0 +1 @@ +from .profile import profile diff --git a/apps/iatlas/api-gitlab/api/telemetry/profile.py b/apps/iatlas/api-gitlab/api/telemetry/profile.py new file mode 100644 index 0000000000..de506130c2 --- /dev/null +++ b/apps/iatlas/api-gitlab/api/telemetry/profile.py @@ -0,0 +1,96 @@ +import cProfile +import datetime +import os +import time +import logging + +from flask import current_app as app + +log = logging.getLogger('profiling') +log.setLevel(logging.DEBUG) + +# usage: @profile("profile_for_func1_001") + + +def profile(name): + def inner(func): + def wrapper(*args, **kwargs): + if not app.config['PROFILE']: + return func(*args, **kwargs) + prof = cProfile.Profile() + retval = prof.runcall(func, *args, **kwargs) + path = app.config['PROFILE_PATH'] + if not os.path.exists(path): + os.makedirs(path) + fname = func.__qualname__ + now = datetime.datetime.utcnow().timestamp() + prof.dump_stats(os.path.join( + path, '{}.{}-{}.profile'.format(name, fname, now))) + return retval + wrapper.__doc__ = func.__doc__ + wrapper.__name__ = func.__name__ + return wrapper + return inner + + +# Not currently compatible with Alpine +""" +import sys +from os.path import expanduser +from line_profiler import LineProfiler + + +def line_profile(f): + lp = LineProfiler() + lp_wrapper = lp(f) + + def wrapper(*args, **kwargs): + val = lp_wrapper(*args, **kwargs) + path = app.config['PROFILE_PATH'] + if not os.path.exists(path): + os.makedirs(path) + fname = '{}.txt'.format(f.__qualname__) + file = open(os.path.join(path, fname, ), 'w', encoding='utf-8') + lp.print_stats(stream=file, output_unit=1e-03) + lp.print_stats(stream=sys.stdout, output_unit=1e-03) + return val + return wrapper +""" + +from sqlalchemy import event +from sqlalchemy.engine import Engine + + +@event.listens_for(Engine, "before_cursor_execute") +def before_cursor_execute(conn, cursor, statement, + parameters, context, executemany): + conn.info.setdefault('query_start_time', []).append(time.time()) + log.debug("Start Query: %s", statement) + + +@event.listens_for(Engine, "after_cursor_execute") +def after_cursor_execute(conn, cursor, statement, + parameters, context, executemany): + total = time.time() - conn.info['query_start_time'].pop(-1) + log.debug("Query Complete!") + log.debug("Total Time: %f", total) + + +import cProfile +import io +import pstats +import contextlib + + +@contextlib.contextmanager +def profiled(): + pr = cProfile.Profile() + pr.enable() + yield + pr.disable() + s = io.StringIO() + ps = pstats.Stats(pr, stream=s).sort_stats('cumulative') + ps.print_stats() + # uncomment this to see who's calling what + # ps.print_callers() + print(s.getvalue()) diff --git a/apps/iatlas/api-gitlab/config.py b/apps/iatlas/api-gitlab/config.py new file mode 100644 index 0000000000..41e252c50e --- /dev/null +++ b/apps/iatlas/api-gitlab/config.py @@ -0,0 +1,79 @@ +from os import environ, path +import logging + + +def get_database_uri(): + HOST = environ['POSTGRES_HOST'] + if 'POSTGRES_PORT' in environ and environ['POSTGRES_PORT'] != 'None': + HOST = HOST + ':' + environ['POSTGRES_PORT'] + POSTGRES = { + 'user': environ['POSTGRES_USER'], + 'pw': environ['POSTGRES_PASSWORD'], + 'db': environ['POSTGRES_DB'], + 'host': HOST, + } + DATABASE_URI = 'postgresql://%(user)s:%(pw)s@%(host)s/%(db)s' % POSTGRES + if 'DATABASE_URI' in environ: + DATABASE_URI = environ['DATABASE_URI'] + return DATABASE_URI + + +BASE_PATH = path.dirname(path.abspath(__file__)) + + +class Config(object): + LOG_APP_NAME = 'iatlas-api' + LOG_COPIES = 10 + LOG_DIR = path.join(BASE_PATH, '.logs', 'development') + LOG_FILE = path.join(LOG_DIR, 'server.log') + LOG_INTERVAL = 1 + LOG_LEVEL = logging.DEBUG + LOG_TIME_INT = 'D' + LOG_TYPE = 'TimedRotatingFile' + LOG_WWW_NAME = 'iatlas-api-access' + PROFILE = True + PROFILE_PATH = path.join(BASE_PATH, '.profiles') + SQLALCHEMY_DATABASE_URI = get_database_uri() + SQLALCHEMY_TRACK_MODIFICATIONS = False + SQLALCHEMY_ENGINE_OPTIONS = {'pool_pre_ping': True} + SQLALCHEMY_ECHO = True + + +class TestConfig(Config): + LOG_DIR = path.join( + BASE_PATH, '.logs', 'test', + environ['FLASK_ENV'] if environ['FLASK_ENV'] != 'test' else '' + ) + LOG_LEVEL = logging.INFO + PROFILE = False + SQLALCHEMY_ECHO = False + SQLALCHEMY_DATABASE_URI = get_database_uri() + TESTING = True + + +class StagingConfig(Config): + LOG_DIR = path.join(BASE_PATH, '.logs', 'staging') + LOG_LEVEL = logging.INFO + LOG_TYPE = 'stream' + PROFILE = False + SQLALCHEMY_ECHO = False + + +class ProdConfig(Config): + LOG_DIR = path.join(BASE_PATH, '.logs', 'production') + LOG_LEVEL = logging.WARN + LOG_TYPE = 'stream' + PROFILE = False + SQLALCHEMY_ECHO = False + + +def get_config(test=False): + FLASK_ENV = environ['FLASK_ENV'] + if (test or FLASK_ENV == 'test'): + return TestConfig + if FLASK_ENV == 'development': + return Config + elif FLASK_ENV == 'staging': + return StagingConfig + else: + return ProdConfig diff --git a/apps/iatlas/api-gitlab/coverage_assets/coverage.css b/apps/iatlas/api-gitlab/coverage_assets/coverage.css new file mode 100644 index 0000000000..e42d16a136 --- /dev/null +++ b/apps/iatlas/api-gitlab/coverage_assets/coverage.css @@ -0,0 +1,50 @@ +body { + font-family: Verdana, Geneva, Tahoma, sans-serif; +} + +.pc_cov { + color: #0b9500; +} + +#index { + margin: 16px 0 0 0; +} + +#index table.index, +table { + margin: 0 auto; +} + +tr:nth-child(even) { + background: #eee; +} + +tr:nth-child(odd) { + background: #fff; +} + +tr.total { + border-top: 2px solid #333; + background: #0b9500; + color: #fff; +} + +#index th.left, +#index td.left { + padding-left: 4px; +} + +#index th.right, +#index td.right { + padding-right: 4px; +} + +@media (prefers-color-scheme: dark) { + tr:nth-child(even) { + background: #5c5c5c; + } + + tr:nth-child(odd) { + background: inherit; + } +} \ No newline at end of file diff --git a/apps/iatlas/api-gitlab/docker-compose.yml b/apps/iatlas/api-gitlab/docker-compose.yml new file mode 100644 index 0000000000..c93e262c60 --- /dev/null +++ b/apps/iatlas/api-gitlab/docker-compose.yml @@ -0,0 +1,38 @@ +version: "3.8" + +services: + api: + env_file: ${DOT_ENV_FILE} + # Ensure specific environment variables are ALWAYS available. + environment: + - FLASK_APP=${FLASK_APP} + - FLASK_ENV=${FLASK_ENV} + - FLASK_RUN_PORT=${FLASK_RUN_PORT} + - POSTGRES_DB=${POSTGRES_DB} + - POSTGRES_HOST=${POSTGRES_HOST} + - POSTGRES_PORT=${POSTGRES_PORT} + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} + - POSTGRES_USER=${POSTGRES_USER} + - PYTHONUNBUFFERED=1 + - SNAKEVIZ_PORT=${SNAKEVIZ_PORT} + build: + context: ./ + dockerfile: Dockerfile-dev + args: + pythonImageVersion: ${PYTHON_IMAGE_VERSION} + container_name: iatlas-api-dev + image: iatlas-api:dev + ports: + - ${FLASK_RUN_PORT}:${FLASK_RUN_PORT} + - ${SNAKEVIZ_PORT}:${SNAKEVIZ_PORT} + volumes: + - .:/project:delegated + - ~/.gitconfig:/root/.gitconfig:delegated + - ~/.ssh:/root/.ssh:delegated + - iatlas-api-dev-root-vol:/root:delegated + logging: + options: + max-size: "10m" + max-file: "3" +volumes: + iatlas-api-dev-root-vol: diff --git a/apps/iatlas/api-gitlab/example_queries/README.md b/apps/iatlas/api-gitlab/example_queries/README.md new file mode 100644 index 0000000000..5174597dc1 --- /dev/null +++ b/apps/iatlas/api-gitlab/example_queries/README.md @@ -0,0 +1,9 @@ +# iAtlas API - Example Queries + +[BACK TO MAIN README](./../README.md) + +[BACK TO TESTING README](./../tests/TESTING.md) + +Each graphql file in the [`example_queries`](./) folder has one or more examples. In the comments at the bottom of each file may be an example of variables to pass. These may be used in the graphiql playground when doing manual testing. + +More examples may be found in the iAtlas application repo [here](https://github.com/CRI-iAtlas/iatlas.api.client/tree/master/inst/queries) diff --git a/apps/iatlas/api-gitlab/example_queries/colocalizations.gql b/apps/iatlas/api-gitlab/example_queries/colocalizations.gql new file mode 100644 index 0000000000..4124278983 --- /dev/null +++ b/apps/iatlas/api-gitlab/example_queries/colocalizations.gql @@ -0,0 +1,73 @@ +query Colocalizations( + $paging: PagingInput + $distinct: Boolean + $dataSet: [String!] + $colocDataSet: [String!] + $feature: [String!] + $entrez: [Int!] + $snp: [String!] + $qtlType: QTLTypeEnum + $eCaviarPP: ECaviarPPEnum + $plotType: ColocPlotTypeEnum +) { + colocalizations( + paging: $paging + distinct: $distinct + dataSet: $dataSet + colocDataSet: $colocDataSet + feature: $feature + entrez: $entrez + snp: $snp + qtlType: $qtlType + eCaviarPP: $eCaviarPP + plotType: $plotType + ) { + paging { + type + pages + total + startCursor + endCursor + hasPreviousPage + hasNextPage + page + limit + } + error + items { + dataSet { + name + display + } + colocDataSet { + name + display + } + feature { + name + display + germline_module + germline_category + } + snp { + name + rsid + bp + chr + } + gene { + entrez + hgnc + } + qtlType + eCaviarPP + tissue + plotType + spliceLoc + plotLink + } + } +} + +# Variables +# {"dataSet": ["TCGA"]} \ No newline at end of file diff --git a/apps/iatlas/api-gitlab/example_queries/copyNumberResults.gql b/apps/iatlas/api-gitlab/example_queries/copyNumberResults.gql new file mode 100644 index 0000000000..faf841ba29 --- /dev/null +++ b/apps/iatlas/api-gitlab/example_queries/copyNumberResults.gql @@ -0,0 +1,133 @@ +query CopyNumberResults( + $paging: PagingInput + $distinct: Boolean + $dataSet: [String!] + $feature: [String!] + $entrez: [Int!] + $tag: [String!] + $direction: DirectionEnum + $minPValue: Float + $maxPValue: Float + $minLog10PValue: Float + $maxLog10PValue: Float + $minMeanNormal: Float + $minMeanCnv: Float + $minTStat: Float +) { + copyNumberResults( + paging: $paging + distinct: $distinct + dataSet: $dataSet + feature: $feature + entrez: $entrez + tag: $tag + direction: $direction + minPValue: $minPValue + maxPValue: $maxPValue + minLog10PValue: $minLog10PValue + maxLog10PValue: $maxLog10PValue + minMeanNormal: $minMeanNormal + minMeanCnv: $minMeanCnv + minTStat: $minTStat + ) { + items { + direction + meanNormal + meanCnv + pValue + log10PValue + tStat + tag { + name + } + gene { + entrez + hgnc + } + } + error + paging { + type + pages + total + startCursor + endCursor + hasPreviousPage + hasNextPage + page + limit + } + } +} + +query CopyNumberResults( + $paging: PagingInput + $distinct: Boolean + $dataSet: [String!] + $feature: [String!] + $entrez: [Int!] + $tag: [String!] + $direction: DirectionEnum + $minPValue: Float + $maxPValue: Float + $minLog10PValue: Float + $maxLog10PValue: Float + $minMeanNormal: Float + $minMeanCnv: Float + $minTStat: Float +) { + copyNumberResults( + paging: $paging + distinct: $distinct + dataSet: $dataSet + feature: $feature + entrez: $entrez + tag: $tag + direction: $direction + minPValue: $minPValue + maxPValue: $maxPValue + minLog10PValue: $minLog10PValue + maxLog10PValue: $maxLog10PValue + minMeanNormal: $minMeanNormal + minMeanCnv: $minMeanCnv + minTStat: $minTStat + ) { + paging { + type + pages + total + startCursor + endCursor + hasPreviousPage + hasNextPage + page + limit + } + error + items { + id + direction + meanNormal + meanCnv + pValue + log10PValue + tStat + dataSet { + name + } + tag { + name + } + gene { + entrez + hgnc + } + feature { + name + } + } + } +} + +# Variables +# {"dataSet": ["TCGA"], "feature": ["frac_altered"]} diff --git a/apps/iatlas/api-gitlab/example_queries/dataSets.gql b/apps/iatlas/api-gitlab/example_queries/dataSets.gql new file mode 100644 index 0000000000..2004fbc0cb --- /dev/null +++ b/apps/iatlas/api-gitlab/example_queries/dataSets.gql @@ -0,0 +1,13 @@ +query DataSets($dataSet: [String!], $sample: [String!], $type: [String!]) { + dataSets(dataSet: $dataSet, sample: $sample, type: $type) { + display + name + samples { + name + } + type + } +} + +# Variables +# {"sample": ["TCGA-21-5787"]} \ No newline at end of file diff --git a/apps/iatlas/api-gitlab/example_queries/driverResults.gql b/apps/iatlas/api-gitlab/example_queries/driverResults.gql new file mode 100644 index 0000000000..092b50ab17 --- /dev/null +++ b/apps/iatlas/api-gitlab/example_queries/driverResults.gql @@ -0,0 +1,102 @@ +query DriverResults( + $dataSet: [String!] + $entrez: [Int!] + $feature: [String!] + $mutationCode: [String!] + $tag: [String!] + $minPValue: Float + $maxPValue: Float + $minLog10PValue: Float + $maxLog10PValue: Float + $minFoldChange: Float + $minLog10FoldChange: Float + $minNumWildTypes: Int + $minNumMutants: Int +) { + driverResults( + dataSet: $dataSet + feature: $feature + entrez: $entrez + mutationCode: $mutationCode + tag: $tag + minPValue: $minPValue + maxPValue: $maxPValue + minLog10PValue: $minLog10PValue + maxLog10PValue: $maxLog10PValue + minFoldChange: $minFoldChange + minLog10FoldChange: $minLog10FoldChange + minNumWildTypes: $minNumWildTypes + minNumMutants: $minNumMutants + ) { + items { + pValue + log10PValue + foldChange + log10FoldChange + numWildTypes + numMutants + } + } +} + +query DriverResults_test( + $dataSet: [String!] + $entrez: [Int!] + $feature: [String!] + $mutationCode: [String!] + $tag: [String!] + $minPValue: Float + $maxPValue: Float + $minLog10PValue: Float + $maxLog10PValue: Float + $minFoldChange: Float + $minLog10FoldChange: Float + $minNumWildTypes: Int + $minNumMutants: Int +) { + driverResults( + dataSet: $dataSet + feature: $feature + entrez: $entrez + mutationCode: $mutationCode + tag: $tag + minPValue: $minPValue + maxPValue: $maxPValue + minLog10PValue: $minLog10PValue + maxLog10PValue: $maxLog10PValue + minFoldChange: $minFoldChange + minLog10FoldChange: $minLog10FoldChange + minNumWildTypes: $minNumWildTypes + minNumMutants: $minNumMutants + ) { + items { + dataSet { + name + display + } + feature { + name + display + } + gene { + entrez + hgnc + } + mutationCode + mutationId + tag { + name + display + } + pValue + foldChange + log10PValue + log10FoldChange + numWildTypes + numMutants + } + } +} + +# Variables +# {"dataSet": ["TCGA"], "tag": ["C1", "C2", "C3", "C4", "C5", "C6"], "feature": ["leukocyte_fraction"], "minNumWildTypes": 30, "minNumMutants": 30} diff --git a/apps/iatlas/api-gitlab/example_queries/edges.gql b/apps/iatlas/api-gitlab/example_queries/edges.gql new file mode 100644 index 0000000000..efc606a87b --- /dev/null +++ b/apps/iatlas/api-gitlab/example_queries/edges.gql @@ -0,0 +1,30 @@ +query Edges( + $maxScore: Float + $minScore: Float + $node1: [String!] + $node2: [String!] + $page: Int +) { + edges( + maxScore: $maxScore + minScore: $minScore + node1: $node1 + node2: $node2 + page: $page + ) { + items { + label + name + score + node1 { + name + } + node2 { + name + } + } + } +} + +# Variables +# {"dataSet": ["TCGA"], "related": ["Immune_Subtype"]} diff --git a/apps/iatlas/api-gitlab/example_queries/features.gql b/apps/iatlas/api-gitlab/example_queries/features.gql new file mode 100644 index 0000000000..f259b05a3e --- /dev/null +++ b/apps/iatlas/api-gitlab/example_queries/features.gql @@ -0,0 +1,28 @@ +query Features( + $dataSet: [String!] + $related: [String!] + $tag: [String!] + $feature: [String!] + $featureClass: [String!] + $sample: [String!] + $minValue: Float + $maxValue: Float +) { + features( + dataSet: $dataSet + related: $related + tag: $tag + feature: $feature + featureClass: $featureClass + sample: $sample + minValue: $minValue + maxValue: $maxValue + ) { + name + valueMax + valueMin + } +} + +# Variables +# {"feature": ["TCR_Shannon"], "dataSet": ["PCAWG"], "related": ["Immune_Subtype"], "minValue": 0.094192693, "maxValue": 5.7561021} \ No newline at end of file diff --git a/apps/iatlas/api-gitlab/example_queries/featuresByClass.gql b/apps/iatlas/api-gitlab/example_queries/featuresByClass.gql new file mode 100644 index 0000000000..204b052c68 --- /dev/null +++ b/apps/iatlas/api-gitlab/example_queries/featuresByClass.gql @@ -0,0 +1,33 @@ +query FeaturesByClass( + $dataSet: [String!] + $related: [String!] + $tag: [String!] + $feature: [String!] + $featureClass: [String!] + $sample: [String!] + $minValue: Float + $maxValue: Float +) { + featuresByClass( + dataSet: $dataSet + related: $related + tag: $tag + feature: $feature + featureClass: $featureClass + sample: $sample + minValue: $minValue + maxValue: $maxValue + ) { + class + features { + name + order + unit + valueMax + valueMin + } + } +} + +# Variables +# {"dataSet": ["TCGA"], "related": ["Immune_Subtype"], "tag": ["C1"], "featureClass": ["TIL Map Characteristic"], "minValue": 0.094192693, "maxValue": 5.7561021} \ No newline at end of file diff --git a/apps/iatlas/api-gitlab/example_queries/featuresByTag.gql b/apps/iatlas/api-gitlab/example_queries/featuresByTag.gql new file mode 100644 index 0000000000..fc009ee6f1 --- /dev/null +++ b/apps/iatlas/api-gitlab/example_queries/featuresByTag.gql @@ -0,0 +1,41 @@ +query FeaturesByTag( + $dataSet: [String!] + $related: [String!] + $tag: [String!] + $feature: [String!] + $featureClass: [String!] + $sample: [String!] + $minValue: Float + $maxValue: Float +) { + featuresByTag( + dataSet: $dataSet + related: $related + tag: $tag + feature: $feature + featureClass: $featureClass + sample: $sample + minValue: $minValue + maxValue: $maxValue + ) { + tag + characteristics + shortDisplay + features { + class + display + methodTag + name + order + samples { name } + valueMin + valueMax + unit + } + } +} + +# Variables +# {"dataSet": ["TCGA"], "related": ["Immune_Subtype"], "feature": ["Det_Ratio"], "minValue": 0.094192693, "maxValue": 5.7561021} +# or +# {"dataSet": ["TCGA"], "related": ["Immune_Subtype"], "feature": ["Det_Ratio"], "featureClass": ["TIL Map Characteristic"]} \ No newline at end of file diff --git a/apps/iatlas/api-gitlab/example_queries/gene.gql b/apps/iatlas/api-gitlab/example_queries/gene.gql new file mode 100644 index 0000000000..c1babe3ccb --- /dev/null +++ b/apps/iatlas/api-gitlab/example_queries/gene.gql @@ -0,0 +1,8 @@ +query Gene($entrez: Int!) { + gene(entrez: $entrez) { + hgnc + } +} + +# Variables +# {"entrez": 3627} \ No newline at end of file diff --git a/apps/iatlas/api-gitlab/example_queries/geneFamilies.gql b/apps/iatlas/api-gitlab/example_queries/geneFamilies.gql new file mode 100644 index 0000000000..52be1376e7 --- /dev/null +++ b/apps/iatlas/api-gitlab/example_queries/geneFamilies.gql @@ -0,0 +1,11 @@ +query geneFamilies($name: [String!]) { + geneFamilies(name: $name) { + genes { + entrez + } + name + } +} + +# Variables +# {"name": ["Receptor"]} \ No newline at end of file diff --git a/apps/iatlas/api-gitlab/example_queries/geneTypes.gql b/apps/iatlas/api-gitlab/example_queries/geneTypes.gql new file mode 100644 index 0000000000..c1600586aa --- /dev/null +++ b/apps/iatlas/api-gitlab/example_queries/geneTypes.gql @@ -0,0 +1,11 @@ +query GeneTypes($name: [String!]) { + geneTypes(name: $name) { + name + genes { + entrez + } + } +} + +# Variables +# {"name": ["B_cells"]} \ No newline at end of file diff --git a/apps/iatlas/api-gitlab/example_queries/genes.gql b/apps/iatlas/api-gitlab/example_queries/genes.gql new file mode 100644 index 0000000000..69c8d7e462 --- /dev/null +++ b/apps/iatlas/api-gitlab/example_queries/genes.gql @@ -0,0 +1,31 @@ +query Genes( + $dataSet: [String!] + $entrez: [Int!] + $geneType: [String!] + $maxRnaSeqExpr: Float + $minRnaSeqExpr: Float + $related: [String!] + $sample: [String!] + $tag: [String!] +) { + genes( + dataSet: $dataSet + entrez: $entrez + geneType: $geneType + maxRnaSeqExpr: $maxRnaSeqExpr + minRnaSeqExpr: $minRnaSeqExpr + related: $related + sample: $sample + tag: $tag + ) { + entrez + publications { + pubmedId + } + samples + rnaSeqExpr + } +} + +# Variables +# {"entrez": [3627, 383, 941, 958], "geneType": ["immunomodulator"], "sample": ["DO219585"]} diff --git a/apps/iatlas/api-gitlab/example_queries/genesByTag.gql b/apps/iatlas/api-gitlab/example_queries/genesByTag.gql new file mode 100644 index 0000000000..7d46b1f670 --- /dev/null +++ b/apps/iatlas/api-gitlab/example_queries/genesByTag.gql @@ -0,0 +1,39 @@ +query GenesByTag( + $dataSet: [String!] + $entrez: [Int!] + $feature: [String!] + $featureClass: [String!] + $geneType: [String!] + $maxRnaSeqExpr: Float + $minRnaSeqExpr: Float + $related: [String!] + $sample: [String!] + $tag: [String!] +) { + genesByTag( + dataSet: $dataSet + entrez: $entrez + feature: $feature + featureClass: $featureClass + geneType: $geneType + maxRnaSeqExpr: $maxRnaSeqExpr + minRnaSeqExpr: $minRnaSeqExpr + related: $related + sample: $sample + tag: $tag + ) { + tag + characteristics + color + shortDisplay + genes { + entrez + geneFamily + samples + rnaSeqExpr + } + } +} + +# Variables +# {"dataSet": ["TCGA"], "related": ["Immune_Subtype"], "tag": ["C1"], "feature": ["Det_Ratio"], "sample": ["TCGA-05-4420"], "entrez": [3627]} diff --git a/apps/iatlas/api-gitlab/example_queries/germlineGwasResults.gql b/apps/iatlas/api-gitlab/example_queries/germlineGwasResults.gql new file mode 100644 index 0000000000..32838232f8 --- /dev/null +++ b/apps/iatlas/api-gitlab/example_queries/germlineGwasResults.gql @@ -0,0 +1,55 @@ +query GermlineGwasResults( + $dataSet: [String!] + $feature: [String!] + $snp: [String!] + $minPValue: Float + $maxPValue: Float + $paging: PagingInput + $distinct: Boolean +) { + germlineGwasResults( + dataSet: $dataSet + feature: $feature + snp: $snp + minPValue: $minPValue + maxPValue: $maxPValue + paging: $paging + distinct: $distinct + ){ + items { + dataSet{ + name + display + } + feature{ + name + display + germline_module + germline_category + } + snp{ + name + rsid + chr + bp + } + pValue + maf + } + paging{ + type + pages + total + page + limit + hasNextPage + hasPreviousPage + startCursor + endCursor + } + error + } +} + +# Variables +# {"dataSet": ["TCGA"]} \ No newline at end of file diff --git a/apps/iatlas/api-gitlab/example_queries/heritabilityResults.gql b/apps/iatlas/api-gitlab/example_queries/heritabilityResults.gql new file mode 100644 index 0000000000..6a7f6b3a8d --- /dev/null +++ b/apps/iatlas/api-gitlab/example_queries/heritabilityResults.gql @@ -0,0 +1,52 @@ +query HeritabilityResults( + $dataSet: [String!] + $feature: [String!] + $cluster: [String!] + $minPValue: Float + $maxPValue: Float + $paging: PagingInput + $distinct: Boolean +) { + heritabilityResults( + dataSet: $dataSet + feature: $feature + cluster: $cluster + minPValue: $minPValue + maxPValue: $maxPValue + paging: $paging + distinct: $distinct + ){ + items { + dataSet{ + name + display + } + feature{ + name + display + germline_module + germline_category + } + pValue + cluster + fdr + variance + se + } + paging{ + type + pages + total + page + limit + hasNextPage + hasPreviousPage + startCursor + endCursor + } + error + } +} + +# Variables +# {"dataSet": ["TCGA"]} diff --git a/apps/iatlas/api-gitlab/example_queries/mutationsBySamples.gql b/apps/iatlas/api-gitlab/example_queries/mutationsBySamples.gql new file mode 100644 index 0000000000..aa002eef97 --- /dev/null +++ b/apps/iatlas/api-gitlab/example_queries/mutationsBySamples.gql @@ -0,0 +1,51 @@ +query MutationsBySample( + $paging: PagingInput + $distinct: Boolean + $entrez: [Int!] + $mutationCode: [String!] + $mutationId: [Int!] + $mutationType: [String!] + $page: Int + $sample: [String!] + $status: [StatusEnum!] +) { + mutationsBySample( + paging: $paging + distinct: $distinct + entrez: $entrez + mutationCode: $mutationCode + mutationId: $mutationId + mutationType: $mutationType + page: $page + sample: $sample + status: $status + ) { + items { + name + mutations { + gene { + entrez + hgnc + } + mutationCode + mutationType { + name + display + } + status + } + } + paging { + type + startCursor + endCursor + limit + page + pages + total + } + } +} + +# Variables +# { "entrez": [207], "sample": ["TCGA-02-0047"] } diff --git a/apps/iatlas/api-gitlab/example_queries/nodes.gql b/apps/iatlas/api-gitlab/example_queries/nodes.gql new file mode 100644 index 0000000000..965de24f05 --- /dev/null +++ b/apps/iatlas/api-gitlab/example_queries/nodes.gql @@ -0,0 +1,80 @@ +query Nodes( + $paging: PagingInput + $distinct: Boolean + $dataSet: [String!] + $entrez: [Int!] + $feature: [String!] + $maxScore: Float + $minScore: Float + $network: [String!] + $related: [String!] + $tag: [String!] +) { + nodes( + paging: $paging + distinct: $distinct + dataSet: $dataSet + entrez: $entrez + feature: $feature + maxScore: $maxScore + minScore: $minScore + network: $network + related: $related + tag: $tag + ) { + paging { + type + pages + total + page + limit + hasNextPage + hasPreviousPage + startCursor + endCursor + } + items { + label + name + score + x + y + dataSet { + name + } + feature { + name + } + gene { + entrez + } + tags { + name + } + } + } +} + +query Nodes_test( + $dataSet: [String!] + $related: [String!] + $network: [String!] +) { + nodes(dataSet: $dataSet, related: $related, network: $network) { + items { + name + tags { + name + } + } + } +} + +# Variables +# { +# "dataSet": ["TCGA"], +# "related": ["Immune_Subtype"], +# "network": ["extracellular_network"], +# "page": 2 +# } +# {"paging": {"first": 100}, "dataSet": ["TCGA"], "related": ["Immune_Subtype"], "network": ["extracellular_network"], "entrez": [100133941]} diff --git a/apps/iatlas/api-gitlab/example_queries/patients.gql b/apps/iatlas/api-gitlab/example_queries/patients.gql new file mode 100644 index 0000000000..6e59d20854 --- /dev/null +++ b/apps/iatlas/api-gitlab/example_queries/patients.gql @@ -0,0 +1,78 @@ +query Patients( + $barcode: [String!] + $dataSet: [String!] + $ethnicity: [EthnicityEnum!] + $gender: [GenderEnum!] + $maxAgeAtDiagnosis: Int + $maxHeight: Float + $maxWeight: Float + $minAgeAtDiagnosis: Int + $minHeight: Float + $minWeight: Float + $race: [RaceEnum!] + $sample: [String!] + $slide: [String!] +) { + patients( + barcode: $barcode + dataSet: $dataSet + ethnicity: $ethnicity + gender: $gender + maxAgeAtDiagnosis: $maxAgeAtDiagnosis + maxHeight: $maxHeight + maxWeight: $maxWeight + minAgeAtDiagnosis: $minAgeAtDiagnosis + minHeight: $minHeight + minWeight: $minWeight + race: $race + sample: $sample + slide: $slide + ) { + samples + slides { + name + } + } +} + +query Patients_test( + $barcode: [String!] + $dataSet: [String!] + $ethnicity: [EthnicityEnum!] + $gender: [GenderEnum!] + $maxAgeAtDiagnosis: Int + $maxHeight: Float + $maxWeight: Float + $minAgeAtDiagnosis: Int + $minHeight: Float + $minWeight: Float + $race: [RaceEnum!] + $sample: [String!] + $slide: [String!] +) { + patients( + barcode: $barcode + dataSet: $dataSet + ethnicity: $ethnicity + gender: $gender + maxAgeAtDiagnosis: $maxAgeAtDiagnosis + maxHeight: $maxHeight + maxWeight: $maxWeight + minAgeAtDiagnosis: $minAgeAtDiagnosis + minHeight: $minHeight + minWeight: $minWeight + race: $race + sample: $sample + slide: $slide + ) { + ageAtDiagnosis + barcode + ethnicity + gender + height + race + weight + } +} + +# Variables diff --git a/apps/iatlas/api-gitlab/example_queries/rareVariantPathwayAssociations.gql b/apps/iatlas/api-gitlab/example_queries/rareVariantPathwayAssociations.gql new file mode 100644 index 0000000000..e6f2b2e468 --- /dev/null +++ b/apps/iatlas/api-gitlab/example_queries/rareVariantPathwayAssociations.gql @@ -0,0 +1,57 @@ +query RareVariantPathwayAssociation( + $paging: PagingInput + $distinct: Boolean + $dataSet: [String!] + $feature: [String!] + $pathway: [String!] + $minPValue: Float + $maxPValue: Float +) { + rareVariantPathwayAssociations( + paging: $paging + distinct: $distinct + dataSet: $dataSet + feature: $feature + pathway: $pathway + minPValue: $minPValue + maxPValue: $maxPValue + ) { + items { + dataSet { + name + display + } + feature { + name + display + germline_module + germline_category + } + pathway + pValue + min + max + mean + q1 + q2 + q3 + nMutants + nTotal + } + paging { + type + pages + total + startCursor + endCursor + hasPreviousPage + hasNextPage + page + limit + } + error + } +} + +# Variables +# {"dataSet": ["TCGA"]} diff --git a/apps/iatlas/api-gitlab/example_queries/related.gql b/apps/iatlas/api-gitlab/example_queries/related.gql new file mode 100644 index 0000000000..1d07632f76 --- /dev/null +++ b/apps/iatlas/api-gitlab/example_queries/related.gql @@ -0,0 +1,11 @@ +query Related($dataSet: [String!], $related: [String!]) { + related(dataSet: $dataSet, related: $related) { + related { + name + display + } + } +} + +# Variables +# {"dataSet": ["TCGA"]} \ No newline at end of file diff --git a/apps/iatlas/api-gitlab/example_queries/samples.gql b/apps/iatlas/api-gitlab/example_queries/samples.gql new file mode 100644 index 0000000000..b30402d7eb --- /dev/null +++ b/apps/iatlas/api-gitlab/example_queries/samples.gql @@ -0,0 +1,35 @@ +query Samples( + $ethnicity: [EthnicityEnum!] + $gender: [GenderEnum!] + $maxAgeAtDiagnosis: Int + $maxHeight: Float + $maxWeight: Float + $minAgeAtDiagnosis: Int + $minHeight: Float + $minWeight: Float + $name: [String!] + $patient: [String!] + $race: [RaceEnum!] +) { + samples( + ethnicity: $ethnicity + gender: $gender + maxAgeAtDiagnosis: $maxAgeAtDiagnosis + maxHeight: $maxHeight + maxWeight: $maxWeight + minAgeAtDiagnosis: $minAgeAtDiagnosis + minHeight: $minHeight + minWeight: $minWeight + name: $name + patient: $patient + race: $race + ) { + name + patient { + barcode + } + } +} + +# Variables +# {"name": ["TCGA-21-5787"]} diff --git a/apps/iatlas/api-gitlab/example_queries/samplesByMutationStatus.gql b/apps/iatlas/api-gitlab/example_queries/samplesByMutationStatus.gql new file mode 100644 index 0000000000..df5c14ac54 --- /dev/null +++ b/apps/iatlas/api-gitlab/example_queries/samplesByMutationStatus.gql @@ -0,0 +1,49 @@ +query SamplesByMutationStatus( + $dataSet: [String!] + $ethnicity: [EthnicityEnum!] + $feature: [String!] + $featureClass: [String!] + $gender: [GenderEnum!] + $maxAgeAtDiagnosis: Int + $maxHeight: Float + $maxWeight: Float + $minAgeAtDiagnosis: Int + $minHeight: Float + $minWeight: Float + $mutationId: [Int!] + $mutationStatus: StatusEnum + $patient: [String!] + $race: [RaceEnum!] + $related: [String!] + $sample: [String!] + $tag: [String!] +) { + samplesByMutationStatus( + dataSet: $dataSet + ethnicity: $ethnicity + feature: $feature + featureClass: $featureClass + gender: $gender + maxAgeAtDiagnosis: $maxAgeAtDiagnosis + maxHeight: $maxHeight + maxWeight: $maxWeight + minAgeAtDiagnosis: $minAgeAtDiagnosis + minHeight: $minHeight + minWeight: $minWeight + mutationId: $mutationId + mutationStatus: $mutationStatus + patient: $patient + race: $race + related: $related + sample: $sample + tag: $tag + ) { + status + samples { + name + } + } +} + +# Variables +# {"sample": ["TCGA-38-7271"]} diff --git a/apps/iatlas/api-gitlab/example_queries/samplesByTag.gql b/apps/iatlas/api-gitlab/example_queries/samplesByTag.gql new file mode 100644 index 0000000000..e1bf292654 --- /dev/null +++ b/apps/iatlas/api-gitlab/example_queries/samplesByTag.gql @@ -0,0 +1,45 @@ +query SamplesByTag( + $dataSet: [String!] + $ethnicity: [EthnicityEnum!] + $feature: [String!] + $featureClass: [String!] + $gender: [GenderEnum!] + $maxAgeAtDiagnosis: Int + $maxHeight: Float + $maxWeight: Float + $minAgeAtDiagnosis: Int + $minHeight: Float + $minWeight: Float + $name: [String!] + $patient: [String!] + $race: [RaceEnum!] + $related: [String!] + $tag: [String!] +) { + samplesByTag( + dataSet: $dataSet + ethnicity: $ethnicity + feature: $feature + featureClass: $featureClass + gender: $gender + maxAgeAtDiagnosis: $maxAgeAtDiagnosis + maxHeight: $maxHeight + maxWeight: $maxWeight + minAgeAtDiagnosis: $minAgeAtDiagnosis + minHeight: $minHeight + minWeight: $minWeight + name: $name + patient: $patient + race: $race + related: $related + tag: $tag + ) { + tag + samples { + name + } + } +} + +# Variables +# {"feature": ["Det_Ratio"], "featureClass": ["TIL Map Characteristic"]} diff --git a/apps/iatlas/api-gitlab/example_queries/slides.gql b/apps/iatlas/api-gitlab/example_queries/slides.gql new file mode 100644 index 0000000000..11b57e641f --- /dev/null +++ b/apps/iatlas/api-gitlab/example_queries/slides.gql @@ -0,0 +1,37 @@ +query Slides( + $barcode: [String!] + $ethnicity: [EthnicityEnum!] + $gender: [GenderEnum!] + $maxAgeAtDiagnosis: Int + $maxHeight: Float + $maxWeight: Float + $minAgeAtDiagnosis: Int + $minHeight: Float + $minWeight: Float + $name: [String!] + $race: [RaceEnum!] + $sample: [String!] +) { + slides( + barcode: $barcode + ethnicity: $ethnicity + gender: $gender + maxAgeAtDiagnosis: $maxAgeAtDiagnosis + maxHeight: $maxHeight + maxWeight: $maxWeight + minAgeAtDiagnosis: $minAgeAtDiagnosis + minHeight: $minHeight + minWeight: $minWeight + name: $name + race: $race + sample: $sample + ) { + name + patient { + ageAtDiagnosis + } + } +} + +# Variables +# {"barcode": ["TCGA-21-5787"]} diff --git a/apps/iatlas/api-gitlab/example_queries/tags.gql b/apps/iatlas/api-gitlab/example_queries/tags.gql new file mode 100644 index 0000000000..cc9173255c --- /dev/null +++ b/apps/iatlas/api-gitlab/example_queries/tags.gql @@ -0,0 +1,32 @@ +query Tags( + $dataSet: [String!] + $related: [String!] + $tag: [String!] + $feature: [String!] + $featureClass: [String!] + $sample: [String!] +) { + tags( + dataSet: $dataSet + related: $related + tag: $tag + feature: $feature + featureClass: $featureClass + sample: $sample + ) { + characteristics + color + shortDisplay + name + related { + name + shortDisplay + characteristics + } + sampleCount + samples + } +} + +# Variables +# {"dataSet": ["TCGA"], "related": ["Immune_Subtype"]} diff --git a/apps/iatlas/api-gitlab/example_queries/test.gql b/apps/iatlas/api-gitlab/example_queries/test.gql new file mode 100644 index 0000000000..1849d7b8ad --- /dev/null +++ b/apps/iatlas/api-gitlab/example_queries/test.gql @@ -0,0 +1,3 @@ +query Test { + test +} \ No newline at end of file diff --git a/apps/iatlas/api-gitlab/git-genui.config.json b/apps/iatlas/api-gitlab/git-genui.config.json new file mode 100644 index 0000000000..ab2ea42ea0 --- /dev/null +++ b/apps/iatlas/api-gitlab/git-genui.config.json @@ -0,0 +1,12 @@ +{ + "project": { + "tracker": { + "name": "PivotalTracker", + "projectId": 2421624 + } + }, + "tracker": { + "name": "PivotalTracker", + "projectId": 2421624 + } +} \ No newline at end of file diff --git a/apps/iatlas/api-gitlab/iatlas-api.code-workspace b/apps/iatlas/api-gitlab/iatlas-api.code-workspace new file mode 100644 index 0000000000..213407d90a --- /dev/null +++ b/apps/iatlas/api-gitlab/iatlas-api.code-workspace @@ -0,0 +1,12 @@ +{ + "folders": [ + { + "path": "." + } + ], + "settings": { + "files.associations": { + "Dockerfile*": "dockerfile" + } + } +} \ No newline at end of file diff --git a/apps/iatlas/api-gitlab/iatlasapi.py b/apps/iatlas/api-gitlab/iatlasapi.py new file mode 100644 index 0000000000..e52aa36612 --- /dev/null +++ b/apps/iatlas/api-gitlab/iatlasapi.py @@ -0,0 +1,3 @@ +from api import create_app, db + +app = create_app() diff --git a/apps/iatlas/api-gitlab/requirements-dev.txt b/apps/iatlas/api-gitlab/requirements-dev.txt new file mode 100644 index 0000000000..a5b9e2c10a --- /dev/null +++ b/apps/iatlas/api-gitlab/requirements-dev.txt @@ -0,0 +1,8 @@ +autopep8==1.5.4 +pylint==2.6.0 +pylint-flask-sqlalchemy==0.2.0 +pytest==6.1.2 +pytest-cov==2.10.1 +pytest-flask-sqlalchemy==1.0.2 +pytest-xdist==2.1.0 +snakeviz==2.1.0 \ No newline at end of file diff --git a/apps/iatlas/api-gitlab/requirements.txt b/apps/iatlas/api-gitlab/requirements.txt new file mode 100644 index 0000000000..966bbab582 --- /dev/null +++ b/apps/iatlas/api-gitlab/requirements.txt @@ -0,0 +1,13 @@ +ariadne==0.13.0 +click==7.1.2 +Flask==1.1.2 +Flask-SQLAlchemy==2.4.3 +graphql-core==3.1.0 +itsdangerous==1.1.0 +Jinja2==2.11.2 +MarkupSafe==1.1.1 +psycopg2-binary==2.8.5 +SQLAlchemy==1.3.17 +starlette==0.13.4 +typing-extensions==3.7.4.2 +Werkzeug==1.0.1 \ No newline at end of file diff --git a/apps/iatlas/api-gitlab/run.py b/apps/iatlas/api-gitlab/run.py new file mode 100644 index 0000000000..93f207c6fe --- /dev/null +++ b/apps/iatlas/api-gitlab/run.py @@ -0,0 +1,23 @@ +import logging +from os import getenv +from api import create_app + +environment = getenv('FLASK_ENV') or 'development' +print(f'Starting server with {environment} config') +app = create_app() + +if __name__ == '__main__': + logger = logging.getLogger(__name__) + HOST = '0.0.0.0' + PORT = getenv('FLASK_RUN_PORT') or '5000' + SSL_ENABLED = False + SSL_CONTEXT = 'adhoc' + + if SSL_ENABLED: + try: + app.run(HOST, PORT, threaded=True, ssl_context=SSL_CONTEXT) + except Exception as e: + logger.error(f'Error: {e}') + logger.info(f'SSL Context: {SSL_CONTEXT}') + else: + app.run(HOST, PORT, threaded=True) diff --git a/apps/iatlas/api-gitlab/set_env_variables.sh b/apps/iatlas/api-gitlab/set_env_variables.sh new file mode 100755 index 0000000000..d4cb93c323 --- /dev/null +++ b/apps/iatlas/api-gitlab/set_env_variables.sh @@ -0,0 +1,39 @@ +#!/bin/bash + +GREEN="\033[0;32m" +YELLOW="\033[1;33m" +# No Color +NC='\033[0m' + +# The project directory. +PROJECT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" +>&2 echo -e "${GREEN}Current project dir - ${PROJECT_DIR}${NC}" + +# .env-dev loading in the shell +DOT_ENV_FILE=${PROJECT_DIR}/.env-dev +dotenv() { + if [ -f "${DOT_ENV_FILE}" ] + then + set -a + [ -f ${DOT_ENV_FILE} ] && . ${DOT_ENV_FILE} + set +a + else + DOT_ENV_FILE=${PROJECT_DIR}/.env-none + >&2 echo -e "${YELLOW}Not using a .env-dev file${NC}" + fi +} +# Run dotenv +dotenv + +# If environment variables are set, use them. If not, use the defaults. +export DOT_ENV_FILE=${DOT_ENV_FILE} +export FLASK_APP=${FLASK_APP:-iatlasapi.py} +export FLASK_ENV=${FLASK_ENV:-development} +export FLASK_RUN_PORT=${FLASK_RUN_PORT:-5000} +export POSTGRES_DB=${POSTGRES_DB:-iatlas_dev} +export POSTGRES_HOST=${POSTGRES_HOST:-172.17.0.1} +export POSTGRES_PORT=${POSTGRES_PORT:-5432} +export POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-docker} +export POSTGRES_USER=${POSTGRES_USER:-postgres} +export PYTHON_IMAGE_VERSION=${PYTHON_IMAGE_VERSION:-3.8.19-alpine3.20} +export SNAKEVIZ_PORT=${SNAKEVIZ_PORT:-8020} diff --git a/apps/iatlas/api-gitlab/setup.cfg b/apps/iatlas/api-gitlab/setup.cfg new file mode 100644 index 0000000000..c7b444164d --- /dev/null +++ b/apps/iatlas/api-gitlab/setup.cfg @@ -0,0 +1,4 @@ +[tool:pytest] +log_cli = true +mocked-sessions=api.db.session +testpaths = tests diff --git a/apps/iatlas/api-gitlab/start.sh b/apps/iatlas/api-gitlab/start.sh new file mode 100755 index 0000000000..783179b619 --- /dev/null +++ b/apps/iatlas/api-gitlab/start.sh @@ -0,0 +1,47 @@ +#!/bin/bash + +# Set the environment variables. +source ./set_env_variables.sh + +build=false + +# If the `-b or --build` flag is passed, set build to true. +while [ ! $# -eq 0 ] +do + case "$1" in + --build | -b) + >&2 echo -e "${GREEN}Build requested${NC}" + build=true + ;; + esac + shift +done + +if [ "${build}" = true ] +then + # Build and start the container. + docker-compose up -d --build +else + # Start the container. + docker-compose up -d +fi + +check_status() { + status_code=$(curl --write-out %{http_code} --silent --output /dev/null localhost:${FLASK_RUN_PORT}/graphiql) + if [[ ${iterator} -lt 35 && ${status_code} -eq 200 ]] + then + >&2 echo -e "${GREEN}GraphiQL is Up at localhost:${FLASK_RUN_PORT}/graphiql${NC}" + open http://localhost:${FLASK_RUN_PORT}/graphiql + elif [[ ${iterator} -eq 35 ]] + then + >&2 echo -e "${YELLOW}Did not work :(${NC}" + else + sleep 1 + ((iterator++)) + check_status + fi +} + +>&2 echo -e "${GREEN}Checking if the server is up at localhost:${FLASK_RUN_PORT} ...${NC}" +iterator=0 +check_status diff --git a/apps/iatlas/api-gitlab/stop.sh b/apps/iatlas/api-gitlab/stop.sh new file mode 100755 index 0000000000..b1a43b0e92 --- /dev/null +++ b/apps/iatlas/api-gitlab/stop.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +# Set the environment variables. +source ./set_env_variables.sh + +# Stop the container. +docker-compose down \ No newline at end of file diff --git a/apps/iatlas/api-gitlab/tests/TESTING.md b/apps/iatlas/api-gitlab/tests/TESTING.md new file mode 100644 index 0000000000..0d77d42068 --- /dev/null +++ b/apps/iatlas/api-gitlab/tests/TESTING.md @@ -0,0 +1,41 @@ +# iAtlas API Testing + +[BACK TO MAIN README](../README.md) + +The app uses [Pytest](https://docs.pytest.org/) for testing. It implement the [pytest-xdist](https://pypi.org/project/pytest-xdist/) plugin for running test in parallel and on multiple cores. + +Coverage is generated using the [pytest-cov](https://pypi.org/project/pytest-cov/) plugin. + +The [`.coveragerc`](./.coveragerc) file is used to configure the coverage generation. + +Additional assets for the coverage generation (ie css, images, etc) are in the [`coverage_assets/`](./coverage_assets/) folder. + +**NOTE:** If running tests outside of the container and the app hasn't been started yet, the environment variables won't have been set yet. To set the environment variables run the following in the same terminal as the tests before executing the tests at the root of the project folder. (Please note the dot(`.`) at the very beginning of the command. This will "source" the script.): + +```sh +. set_env_variables.sh +``` + +To run a test module simple run: + +```sh +pytest path/to/the/test_file.py -n auto +``` + +An individual test may be run in the same manner with: + +```sh +pytest path/to/the/test_file.py::name_of_test_function -n auto +``` + +To generate coverage html run: + +```sh +pytest --cov --cov-report html -n auto +``` + +The `-n auto` at the end of each command is for running on multiple cores. `auto` will automatically determine the number of cores to use. Otherwise, one may specify the number explicitly. + +## Example Queries + +See: [README.md](./../example_queries/README.md) in the [`example_queries`](./../example_queries/) folder diff --git a/apps/iatlas/api-gitlab/tests/__init__.py b/apps/iatlas/api-gitlab/tests/__init__.py new file mode 100644 index 0000000000..9da06141ce --- /dev/null +++ b/apps/iatlas/api-gitlab/tests/__init__.py @@ -0,0 +1 @@ +NoneType = type(None) diff --git a/apps/iatlas/api-gitlab/tests/conftest.py b/apps/iatlas/api-gitlab/tests/conftest.py new file mode 100644 index 0000000000..01b6999624 --- /dev/null +++ b/apps/iatlas/api-gitlab/tests/conftest.py @@ -0,0 +1,349 @@ +import pytest +from api import create_app, db +import json + + +@pytest.fixture(autouse=True) +def enable_transactional_tests(db_session): + """ + Automatically enable transactions for all tests, without importing any extra fixtures. + """ + pass + + +@pytest.fixture(scope='session') +def app(): + app = create_app(test=True) + app.test_request_context().push() + + yield app + db.session.remove() + + +@pytest.fixture(scope='session') +def client(app): + with app.test_client() as client: + yield client + + +@pytest.fixture(scope='session') +def test_db(app): + from api import db + yield db + + +@pytest.fixture(scope='session') +def _db(test_db): + yield test_db + test_db.session.remove() + + +@pytest.fixture(scope='session') +def data_set(): + return 'TCGA' + + +@pytest.fixture(scope='session') +def data_set_id(test_db, data_set): + from api.db_models import Dataset + (id, ) = test_db.session.query(Dataset.id).filter_by( + name=data_set).one_or_none() + return id + + +@pytest.fixture(scope='session') +def pcawg_data_set(): + return 'PCAWG' + + +@pytest.fixture(scope='session') +def pcawg_data_set_id(test_db, pcawg_data_set): + from api.db_models import Dataset + (id, ) = test_db.session.query(Dataset.id).filter_by( + name=pcawg_data_set).one_or_none() + return id + + +@ pytest.fixture(scope='session') +def related(): + return 'Immune_Subtype' + + +@ pytest.fixture(scope='session') +def related_id(test_db, related): + from api.db_models import Tag + (id, ) = test_db.session.query(Tag.id).filter_by( + name=related).one_or_none() + return id + + +@ pytest.fixture(scope='session') +def related3(): + return 'TCGA_Subtype' + + +@ pytest.fixture(scope='session') +def related_id3(test_db, related3): + from api.db_models import Tag + (id, ) = test_db.session.query(Tag.id).filter_by( + name=related3).one_or_none() + return id + + +@ pytest.fixture(scope='session') +def tag(): + return 'C1' + + +@ pytest.fixture(scope='session') +def tag_id(test_db, tag): + from api.db_models import Tag + (id, ) = test_db.session.query(Tag.id).filter_by( + name=tag).one_or_none() + return id + + +@ pytest.fixture(scope='session') +def tag2(): + return 'male' + + +@ pytest.fixture(scope='session') +def tag_id2(test_db, tag2): + from api.db_models import Tag + (id, ) = test_db.session.query(Tag.id).filter_by( + name=tag2).one_or_none() + return id + + +@ pytest.fixture(scope='session') +def tag_i2d(test_db, tag2): + from api.db_models import Tag + (id, ) = test_db.session.query(Tag.id).filter_by( + name=tag2).one_or_none() + return id + + +@ pytest.fixture(scope='session') +def feature_class(): + return 'TIL Map Characteristic' + + +@ pytest.fixture(scope='session') +def feature_class2(): + return 'DNA Alteration' + +@ pytest.fixture(scope='session') +def feature_class2_feature_names(test_db, feature_class2): + from api.db_models import Feature + names = test_db.session.query(Feature.name).filter_by(feature_class=feature_class2).all() + names = [name[0] for name in names] + return names + + +@ pytest.fixture(scope='session') +def entrez_id(): + return 3627 + + +@pytest.fixture(scope='session') +def gene_id(test_db, entrez_id): + from api.db_models import Gene + (id, ) = test_db.session.query(Gene.id).filter_by(entrez_id=entrez_id).one_or_none() + return id + + +@ pytest.fixture(scope='session') +def hgnc_id(test_db, entrez_id): + from api.db_models import Gene + (hgnc_id, ) = test_db.session.query( + Gene.hgnc_id).filter_by(entrez_id=entrez_id).one_or_none() + return hgnc_id + + +@ pytest.fixture(scope='session') +def mutation_type(): + return 'driver_mutation' + + +# Sample id 617 +@ pytest.fixture(scope='session') +def sample(): + return 'TCGA-05-4420' + + +@ pytest.fixture(scope='session') +def sample_id(test_db, sample): + from api.db_models import Sample + (id, ) = test_db.session.query(Sample.id).filter_by(name=sample).one_or_none() + return id + + +@ pytest.fixture(scope='session') +def slide(): + return 'TCGA-05-4244-01Z-00-DX1' + + +# Patient id 617 +@ pytest.fixture(scope='session') +def patient(): + return 'TCGA-05-4420' + + +@ pytest.fixture(scope='session') +def max_age_at_diagnosis(): + return 86 + + +@ pytest.fixture(scope='session') +def min_age_at_diagnosis(): + return 18 + + +@ pytest.fixture(scope='session') +def ethnicity(): + return 'not hispanic or latino' + + +@ pytest.fixture(scope='session') +def gender(): + return 'female' + + +@ pytest.fixture(scope='session') +def max_height(): + return 179 + + +@ pytest.fixture(scope='session') +def min_height(): + return 130 + + +@ pytest.fixture(scope='session') +def race(): + return 'black or african american' + + +@ pytest.fixture(scope='session') +def max_weight(): + return 160 + + +@ pytest.fixture(scope='session') +def min_weight(): + return 42 + +# ---- + + +@pytest.fixture(scope='module') +def cohort_query_builder(): + def f(query_fields): + return """ + query Cohorts( + $paging: PagingInput + $distinct:Boolean + $cohort: [String!] + $dataSet: [String!] + $tag: [String!] + ) { + cohorts( + paging: $paging + distinct: $distinct + cohort: $cohort + dataSet: $dataSet + tag: $tag + ) + """ + query_fields + "}" + return f + + +@pytest.fixture(scope='module') +def cohort_query(cohort_query_builder): + return cohort_query_builder( + """ + { + items { + name + samples { + name + } + } + } + """ + ) + + +@pytest.fixture(scope='module') +def tcga_tag_cohort_name(): + return 'TCGA_TCGA_Subtype' + + +@pytest.fixture(scope='module') +def pcawg_cohort_name(): + return('PCAWG') + + +@pytest.fixture(scope='module') +def tcga_tag_cohort_id(test_db, tcga_tag_cohort_name): + from api.db_models import Cohort + (id, ) = test_db.session.query(Cohort.id).filter_by( + name=tcga_tag_cohort_name).one_or_none() + return id + + +@pytest.fixture(scope='module') +def pcawg_cohort_id(test_db, pcawg_cohort_name): + from api.db_models import Cohort + (id, ) = test_db.session.query(Cohort.id).filter_by( + name=pcawg_cohort_name).one_or_none() + return id + + +@pytest.fixture(scope='module') +def tcga_tag_cohort_samples(client, tcga_tag_cohort_name, cohort_query): + response = client.post('/api', json={'query': cohort_query, 'variables': { + 'cohort': [tcga_tag_cohort_name] + }}) + json_data = json.loads(response.data) + page = json_data['data']['cohorts'] + cohort = page['items'][0] + samples = cohort['samples'] + names = [sample['name'] for sample in samples] + return names + + +@pytest.fixture(scope='module') +def pcawg_cohort_samples(client, pcawg_cohort_name, cohort_query): + response = client.post('/api', json={'query': cohort_query, 'variables': { + 'cohort': [pcawg_cohort_name] + }}) + json_data = json.loads(response.data) + page = json_data['data']['cohorts'] + cohort = page['items'][0] + samples = cohort['samples'] + names = [sample['name'] for sample in samples] + return names + +# for testing germline fields ---- + + +@pytest.fixture(scope='module') +def germline_feature(): + return 'BCR_Richness' + + +@pytest.fixture(scope='module') +def germline_pathway(): + return 'MMR' + + +@pytest.fixture(scope='module') +def germline_category(): + return 'Adaptive Receptor' + + +@pytest.fixture(scope='module') +def germline_module(): + return 'Unassigned' diff --git a/apps/iatlas/api-gitlab/tests/db_models/test_Cell.py b/apps/iatlas/api-gitlab/tests/db_models/test_Cell.py new file mode 100644 index 0000000000..59782e5a7b --- /dev/null +++ b/apps/iatlas/api-gitlab/tests/db_models/test_Cell.py @@ -0,0 +1,13 @@ +import pytest +from api.database import return_cell_query + +def test_cell_query(): + query = return_cell_query() + results = query.limit(3).all() + assert isinstance(results, list) + assert len(results) == 3 + for cell in results: + assert isinstance(cell.name, str) + assert isinstance(cell.id, str) + assert isinstance(cell.cell_type, str) + assert repr(cell).startswith(" 0 + for result in results: + colocalization_id = result.id + string_representation = '' % colocalization_id + assert type(result.data_set) is NoneType + assert type(result.feature) is NoneType + assert type(result.snp) is NoneType + assert type(result.gene) is NoneType + assert result.dataset_id == data_set_id + assert result.feature_id == coloc_feature_id + assert result.snp_id == coloc_snp_id + assert result.gene_id == coloc_gene_id + assert type(result.qtl_type) is str + assert type(result.ecaviar_pp) is str or NoneType + assert type(result.plot_type) is str or NoneType + assert type(result.splice_loc) is str or NoneType + assert type(result.splice_loc) is str or NoneType + assert type(result.link) is str or NoneType + assert repr(result) == string_representation diff --git a/apps/iatlas/api-gitlab/tests/db_models/test_CopyNumberResult.py b/apps/iatlas/api-gitlab/tests/db_models/test_CopyNumberResult.py new file mode 100644 index 0000000000..7329d81f2a --- /dev/null +++ b/apps/iatlas/api-gitlab/tests/db_models/test_CopyNumberResult.py @@ -0,0 +1,88 @@ +import pytest +from sqlalchemy import and_ +from tests import NoneType +from api.database import return_copy_number_result_query +from api.enums import direction_enum + + +@pytest.fixture(scope='module') +def cnr_feature(): + return 'EPIC_NK_Cells' + + +@pytest.fixture(scope='module') +def cnr_tag(): + return 'BLCA' + + +@pytest.fixture(scope='module') +def feature_id(test_db, cnr_feature): + from api.db_models import Feature + (id, ) = test_db.session.query(Feature.id).filter_by( + name=cnr_feature).one_or_none() + return id + + +@pytest.fixture(scope='module') +def cnr_tag_id(test_db, cnr_tag): + from api.db_models import Tag + (id, ) = test_db.session.query(Tag.id).filter_by( + name=cnr_tag).one_or_none() + return id + + +def test_CopyNumberResult_with_relations(app, data_set_id, entrez_id, gene_id, cnr_tag, cnr_tag_id): + string_representation_list = [] + separator = ', ' + relationships_to_join = ['data_set', 'feature', 'gene', 'tag'] + + query = return_copy_number_result_query(*relationships_to_join) + results = query.filter_by(dataset_id=data_set_id).filter_by( + gene_id=gene_id).filter_by(tag_id=cnr_tag_id).limit(3).all() + + assert isinstance(results, list) + for result in results: + copy_number_result_id = result.id + string_representation = '' % copy_number_result_id + string_representation_list.append(string_representation) + assert result.feature.id == result.feature_id + assert result.data_set.id == data_set_id + assert result.gene.entrez_id == entrez_id + assert result.gene.id == gene_id + assert result.tag.id == result.tag_id + assert result.tag.name == cnr_tag + assert result.gene_id == gene_id + assert result.dataset_id == data_set_id + assert type(result.feature_id) is str + assert result.direction in direction_enum.enums + assert type(result.mean_normal) is float or NoneType + assert type(result.mean_cnv) is float or NoneType + assert type(result.p_value) is float or NoneType + assert type(result.log10_p_value) is float or NoneType + assert type(result.t_stat) is float or NoneType + assert repr(result) == string_representation + assert repr(results) == '[' + separator.join( + string_representation_list) + ']' + + +def test_CopyNumberResult_no_relations(app, data_set_id, gene_id, cnr_tag_id, cnr_tag): + query = return_copy_number_result_query() + results = query.filter_by(dataset_id=data_set_id).filter_by( + gene_id=gene_id).filter_by(tag_id=cnr_tag_id).limit(3).all() + + assert isinstance(results, list) + for result in results: + assert type(result.data_set) is NoneType + assert type(result.feature) is NoneType + assert type(result.gene) is NoneType + assert type(result.tag) is NoneType + assert type(result.dataset_id) is str + assert type(result.feature_id) is str + assert result.gene_id == gene_id + assert type(result.tag_id) is str + assert result.direction in direction_enum.enums + assert type(result.mean_normal) is float or NoneType + assert type(result.mean_cnv) is float or NoneType + assert type(result.p_value) is float or NoneType + assert type(result.log10_p_value) is float or NoneType + assert type(result.t_stat) is float or NoneType diff --git a/apps/iatlas/api-gitlab/tests/db_models/test_Dataset.py b/apps/iatlas/api-gitlab/tests/db_models/test_Dataset.py new file mode 100644 index 0000000000..83bbda03a3 --- /dev/null +++ b/apps/iatlas/api-gitlab/tests/db_models/test_Dataset.py @@ -0,0 +1,64 @@ +import pytest +from tests import NoneType +from api.database import return_dataset_query + + +def test_dataset_with_samples(app, data_set): + query = return_dataset_query('samples') + result = query.filter_by(name=data_set).first() + + assert isinstance(result.samples, list) + assert len(result.samples) > 0 + # Don't need to iterate through every result. + for sample in result.samples[0:2]: + assert type(sample.name) is str + assert result.name == data_set + assert type(result.display) is str or NoneType + assert repr(result) == '' % data_set + + +def test_dataset_with_data_set_sample_assoc(app, data_set): + query = return_dataset_query('dataset_sample_assoc') + result = query.filter_by(name=data_set).first() + + assert isinstance(result.dataset_sample_assoc, list) + assert len(result.dataset_sample_assoc) > 0 + # Don't need to iterate through every result. + for dataset_sample_rel in result.dataset_sample_assoc[0:2]: + assert dataset_sample_rel.dataset_id == result.id + + +def test_dataset_with_tags(app, data_set): + query = return_dataset_query('tags') + result = query.filter_by(name=data_set).first() + + assert isinstance(result.tags, list) + assert len(result.tags) > 0 + # Don't need to iterate through every result. + for tag in result.tags[0:2]: + assert type(tag.name) is str + assert result.name == data_set + assert type(result.display) is str or NoneType + assert repr(result) == '' % data_set + + +def test_dataset_with_dataset_sample_assoc(app, data_set): + query = return_dataset_query('dataset_tag_assoc') + result = query.filter_by(name=data_set).first() + + assert isinstance(result.dataset_tag_assoc, list) + assert len(result.dataset_tag_assoc) > 0 + # Don't need to iterate through every result. + for dataset_tag_rel in result.dataset_tag_assoc[0:2]: + assert dataset_tag_rel.dataset_id == result.id + + +def test_dataset_no_relations(app, data_set): + query = return_dataset_query() + result = query.filter_by(name=data_set).first() + + assert result.dataset_sample_assoc == [] + assert result.samples == [] + assert type(result.id) is str + assert result.name == data_set + assert type(result.display) is str or NoneType diff --git a/apps/iatlas/api-gitlab/tests/db_models/test_DatasetToSample.py b/apps/iatlas/api-gitlab/tests/db_models/test_DatasetToSample.py new file mode 100644 index 0000000000..e7e78e36e0 --- /dev/null +++ b/apps/iatlas/api-gitlab/tests/db_models/test_DatasetToSample.py @@ -0,0 +1,40 @@ +import pytest +from api.database import return_dataset_to_sample_query + + +def test_DatasetToSample_with_relations(app, data_set_id): + relationships_to_join = ['data_sets', 'samples'] + + query = return_dataset_to_sample_query(*relationships_to_join) + results = query.filter_by(dataset_id=data_set_id).limit(3).all() + + assert isinstance(results, list) + assert len(results) > 0 + for result in results: + assert result.dataset_id == data_set_id + assert isinstance(result.data_sets, list) + assert len(result.data_sets) == 1 + for data_set in result.data_sets: + assert type(data_set.name) is str + assert isinstance(result.samples, list) + assert len(result.samples) > 0 + # Don't need to iterate through every result. + for sample in result.samples[0:2]: + assert type(sample.id) is str + assert type(sample.name) is str + assert type(result.sample_id) is str + assert repr(result) == '' % data_set_id + assert repr(results) == '[]' % data_set_id + + +def test_DatasetToSample_no_relations(app, data_set_id): + query = return_dataset_to_sample_query() + results = query.filter_by(dataset_id=data_set_id).limit(3).all() + + assert isinstance(results, list) + assert len(results) == 1 + for result in results: + assert result.data_sets == [] + assert result.samples == [] + assert result.dataset_id == data_set_id + assert type(result.sample_id) is str diff --git a/apps/iatlas/api-gitlab/tests/db_models/test_DatasetToTag.py b/apps/iatlas/api-gitlab/tests/db_models/test_DatasetToTag.py new file mode 100644 index 0000000000..5efa2d4f4b --- /dev/null +++ b/apps/iatlas/api-gitlab/tests/db_models/test_DatasetToTag.py @@ -0,0 +1,40 @@ +import pytest +from tests import NoneType +from api.database import return_dataset_to_tag_query + + +def test_DatasetToTag_with_relations(app, data_set, data_set_id): + relationships_to_join = ['data_sets', 'tags'] + + query = return_dataset_to_tag_query(*relationships_to_join) + results = query.filter_by(dataset_id=data_set_id).limit(3).all() + + assert isinstance(results, list) + assert len(results) == 1 + for result in results: + data_sets = result.data_sets + tags = result.tags + assert result.dataset_id == data_set_id + assert isinstance(data_sets, list) + assert len(data_sets) == 1 + for current_data_set in data_sets: + assert current_data_set.name == data_set + assert isinstance(tags, list) + assert len(tags) > 0 + for tag in tags: + assert type(tag.name) is str + assert repr(result) == '' % data_set_id + assert repr(results) == '[]' % data_set_id + + +def test_DatasetToTag_no_relations(app, data_set_id): + query = return_dataset_to_tag_query() + results = query.filter_by(dataset_id=data_set_id).limit(3).all() + + assert isinstance(results, list) + assert len(results) == 1 + for result in results: + assert result.data_sets == [] + assert result.tags == [] + assert result.dataset_id == data_set_id + assert type(result.tag_id) is str diff --git a/apps/iatlas/api-gitlab/tests/db_models/test_DriverResult.py b/apps/iatlas/api-gitlab/tests/db_models/test_DriverResult.py new file mode 100644 index 0000000000..4d4d0a093d --- /dev/null +++ b/apps/iatlas/api-gitlab/tests/db_models/test_DriverResult.py @@ -0,0 +1,106 @@ +import pytest +from tests import NoneType +from api.database import return_driver_result_query + + +@pytest.fixture(scope='module') +def dr_feature(test_db): + return 'Mast_cells_resting' + + +@pytest.fixture(scope='module') +def dr_feature_id(test_db, dr_feature): + from api.db_models import Feature + (id, ) = test_db.session.query(Feature.id).filter_by( + name=dr_feature).one_or_none() + return id + + +@pytest.fixture(scope='module') +def dr_mutation(): + return 'ABL1:(NS)' + + +@pytest.fixture(scope='module') +def dr_mutation_id(test_db, dr_mutation): + from api.db_models import Mutation + (id, ) = test_db.session.query(Mutation.id).filter_by( + name=dr_mutation).one_or_none() + return id + + +@pytest.fixture(scope='module') +def dr_code(): + return '(NS)' + + +@pytest.fixture(scope='module') +def dr_tag(): + return 'BLCA' + + +@pytest.fixture(scope='module') +def dr_tag_id(test_db, dr_tag): + from api.db_models import Tag + (id, ) = test_db.session.query(Tag.id).filter_by( + name=dr_tag).one_or_none() + return id + + +def test_DriverResult_with_relations(app, data_set, data_set_id, dr_feature, dr_feature_id, dr_mutation, dr_tag, dr_tag_id, dr_mutation_id): + string_representation_list = [] + separator = ', ' + relationships_to_join = ['data_set', 'feature', 'mutation', 'tag'] + + query = return_driver_result_query(*relationships_to_join) + results = query.filter_by(dataset_id=data_set_id).filter_by( + feature_id=dr_feature_id).filter_by( + mutation_id=dr_mutation_id).filter_by(tag_id=dr_tag_id).limit(3).all() + + assert isinstance(results, list) + assert len(results) > 0 + for result in results[0:2]: + driver_result_id = result.id + string_representation = '' % driver_result_id + string_representation_list.append(string_representation) + assert result.data_set.id == data_set_id + assert result.data_set.name == data_set + assert result.feature.id == dr_feature_id + assert result.feature.name == dr_feature + assert result.mutation.id == dr_mutation_id + assert result.mutation.name == dr_mutation + assert result.tag.id == dr_tag_id + assert result.tag.name == dr_tag + assert type(result.p_value) is float or NoneType + assert type(result.fold_change) is float or NoneType + assert type(result.log10_p_value) is float or NoneType + assert type(result.log10_fold_change) is float or NoneType + assert type(result.n_mutants) is int or NoneType + assert type(result.n_wildtype) is int or NoneType + assert repr(result) == string_representation + assert repr(results) == '[' + separator.join( + string_representation_list) + ']' + + +def test_DriverResult_no_relations(app, data_set_id, dr_feature_id, dr_mutation_id, dr_tag_id): + query = return_driver_result_query() + results = query.filter_by(dataset_id=data_set_id).filter_by( + feature_id=dr_feature_id).filter_by( + mutation_id=dr_mutation_id).filter_by(tag_id=dr_tag_id).limit(3).all() + + assert isinstance(results, list) + assert len(results) > 0 + for result in results: + assert type(result.data_set) is NoneType + assert type(result.feature) is NoneType + assert type(result.mutation) is NoneType + assert type(result.tag) is NoneType + assert result.dataset_id == data_set_id + assert result.feature_id == dr_feature_id + assert result.tag_id == dr_tag_id + assert type(result.p_value) is float or NoneType + assert type(result.fold_change) is float or NoneType + assert type(result.log10_p_value) is float or NoneType + assert type(result.log10_fold_change) is float or NoneType + assert type(result.n_mutants) is int or NoneType + assert type(result.n_wildtype) is int or NoneType diff --git a/apps/iatlas/api-gitlab/tests/db_models/test_Edge.py b/apps/iatlas/api-gitlab/tests/db_models/test_Edge.py new file mode 100644 index 0000000000..162adb5d3a --- /dev/null +++ b/apps/iatlas/api-gitlab/tests/db_models/test_Edge.py @@ -0,0 +1,56 @@ +import pytest +from tests import NoneType +from api.database import return_edge_query + + +@pytest.fixture(scope='module') +def node_1(): + return 'TCGA_cellimage_network_ACC_940' + + +@pytest.fixture(scope='module') +def node_1_id(test_db, node_1): + from api.db_models import Node + (id, ) = test_db.session.query(Node.id).filter_by( + name=node_1).one_or_none() + return id + + +def test_Edge_with_relations(app, node_1, node_1_id): + string_representation_list = [] + separator = ', ' + relationships_to_join = ['node_1', 'node_2'] + + query = return_edge_query(*relationships_to_join) + results = query.filter_by(node_1_id=node_1_id).all() + + assert isinstance(results, list) + for result in results: + string_representation = '' % result.id + string_representation_list.append(string_representation) + assert result.node_1.id == node_1_id + assert result.node_1.name == node_1 + assert result.node_2.id == result.node_2_id + assert result.node_1_id == node_1_id + assert type(result.node_2_id) is str + assert type(result.name) is str + assert type(result.label) is str or NoneType + assert type(result.score) is float or NoneType + assert repr(result) == string_representation + assert repr(results) == '[' + separator.join( + string_representation_list) + ']' + + +def test_Edge_no_relations(app, node_1_id): + query = return_edge_query() + results = query.filter_by(node_1_id=node_1_id).limit(3).all() + + assert isinstance(results, list) + for result in results: + assert type(result.node_1) is NoneType + assert type(result.node_2) is NoneType + assert result.node_1_id == node_1_id + assert type(result.node_2_id) is str + assert type(result.name) is str + assert type(result.label) is str or NoneType + assert type(result.score) is float or NoneType diff --git a/apps/iatlas/api-gitlab/tests/db_models/test_Feature.py b/apps/iatlas/api-gitlab/tests/db_models/test_Feature.py new file mode 100644 index 0000000000..69a2747f91 --- /dev/null +++ b/apps/iatlas/api-gitlab/tests/db_models/test_Feature.py @@ -0,0 +1,95 @@ +import pytest +from tests import NoneType +from api.database import return_feature_query +from api.enums import unit_enum + + +@pytest.fixture(scope='module') +def display(): + return 'Eosinophils' + + +@pytest.fixture(scope='module') +def name(): + return 'Eosinophils' + + +@pytest.fixture(scope='module') +def unit(): + return 'Fraction' + +@pytest.fixture(scope='module') +def method_tag(): + return 'CIBERSORT' + + +def test_Feature_with_relations(app, display, name, unit, method_tag): + relationships_to_join = ['feature_class', 'method_tag', 'samples'] + + query = return_feature_query(*relationships_to_join) + result = query.filter_by(name=name).first() + + if result.samples: + assert isinstance(result.samples, list) + # Don't need to iterate through every result. + for sample in result.samples[0:2]: + assert type(sample.name) is str + assert result.name == name + assert result.display == display + assert result.unit == unit + assert result.method_tag == method_tag + assert type(result.feature_class) == str + assert type(result.germline_category) is str or NoneType + assert type(result.germline_module) is str or NoneType + assert result.unit in unit_enum.enums or type(result.unit) is NoneType + assert repr(result) == '' % name + + +def test_Feature_with_copy_number_results(app, name): + query = return_feature_query('copy_number_results') + result = query.filter_by(name=name).first() + + if result.copy_number_results: + assert isinstance(result.copy_number_results, list) + # Don't need to iterate through every result. + for copy_number_result in result.copy_number_results[0:2]: + assert copy_number_result.feature_id == result.id + + +def test_Feature_with_driver_results(app, name): + query = return_feature_query('driver_results') + result = query.filter_by(name=name).first() + + if result.driver_results: + assert isinstance(result.driver_results, list) + # Don't need to iterate through every result. + for driver_result in result.driver_results[0:2]: + assert driver_result.feature_id == result.id + + +def test_Feature_with_feature_sample_assoc(app, name): + query = return_feature_query('feature_sample_assoc') + result = query.filter_by(name=name).first() + + if result.feature_sample_assoc: + assert isinstance(result.feature_sample_assoc, list) + # Don't need to iterate through every result. + for feature_sample_rel in result.feature_sample_assoc[0:2]: + assert feature_sample_rel.feature_id == result.id + + +def test_Feature_no_relations(app, display, name): + query = return_feature_query() + result = query.filter_by(name=name).first() + + assert type(result.feature_class) is str or NoneType + assert type(result.method_tag) is str or NoneType + assert result.samples == [] + assert result.copy_number_results == [] + assert result.driver_results == [] + assert result.feature_sample_assoc == [] + assert result.name == name + assert result.display == display + assert type(result.germline_category) is str or NoneType + assert type(result.germline_module) is str or NoneType + assert result.unit in unit_enum.enums or type(result.unit) is NoneType diff --git a/apps/iatlas/api-gitlab/tests/db_models/test_FeatureToSample.py b/apps/iatlas/api-gitlab/tests/db_models/test_FeatureToSample.py new file mode 100644 index 0000000000..1c0cb677a2 --- /dev/null +++ b/apps/iatlas/api-gitlab/tests/db_models/test_FeatureToSample.py @@ -0,0 +1,63 @@ +import pytest +from tests import NoneType +from decimal import Decimal +from api.database import return_feature_to_sample_query + + +@pytest.fixture(scope='module') +def fs_feature(test_db): + return 'AS' + + +@pytest.fixture(scope='module') +def fs_feature_id(test_db, fs_feature): + from api.db_models import Feature + (id, ) = test_db.session.query(Feature.id).filter_by( + name=fs_feature).one_or_none() + return id + + +def test_FeatureToSample_with_relations(app, fs_feature, fs_feature_id): + string_representation_list = [] + separator = ', ' + relationships_to_join = ['features', 'samples'] + + query = return_feature_to_sample_query(*relationships_to_join) + results = query.filter_by(feature_id=fs_feature_id).limit(3).all() + + assert isinstance(results, list) + for result in results: + string_representation = '' % fs_feature_id + string_representation_list.append(string_representation) + assert isinstance(result.features, list) + assert len(result.features) > 0 + # Don't need to iterate through every result. + for feature in result.features[0:2]: + assert feature.id == fs_feature_id + assert feature.name == fs_feature + if result.samples: + assert isinstance(result.samples, list) + # Don't need to iterate through every result. + for sample in result.samples[0:2]: + assert type(sample.name) is str + assert type(result.id) is str + assert result.feature_id == fs_feature_id + assert type(result.sample_id) is str + assert isinstance(result.feature_to_sample_value, Decimal) + assert repr(result) == string_representation + assert repr(results) == '[' + separator.join( + string_representation_list) + ']' + + +def test_FeatureToSample_no_relations(app, fs_feature_id): + query = return_feature_to_sample_query() + results = query.filter_by(feature_id=fs_feature_id).limit(3).all() + + assert isinstance(results, list) + for result in results: + assert result.features == [] + assert result.samples == [] + assert result.feature_id == fs_feature_id + assert type(result.id) is str + assert type(result.sample_id) is str + assert isinstance(result.feature_to_sample_value, Decimal) diff --git a/apps/iatlas/api-gitlab/tests/db_models/test_Gene.py b/apps/iatlas/api-gitlab/tests/db_models/test_Gene.py new file mode 100644 index 0000000000..0a025ee024 --- /dev/null +++ b/apps/iatlas/api-gitlab/tests/db_models/test_Gene.py @@ -0,0 +1,131 @@ +import pytest +from tests import NoneType +from api.database import return_gene_query +from api.db_models import Gene + + +def test_Gene_with_relations(app, entrez_id, hgnc_id): + relationships_to_join = ['gene_family', + 'gene_function', + 'gene_sets', + 'immune_checkpoint', + 'gene_pathway', + 'samples', + 'super_category', + 'therapy_type'] + + query = return_gene_query(*relationships_to_join) + result = query.filter_by(entrez_id=entrez_id).one_or_none() + + assert result + #assert result.gene_set_assoc == [] + if result.gene_sets: + assert isinstance(result.gene_sets, list) + for gene_set in result.gene_sets[0:2]: + assert type(gene_set.name) is str + if result.samples: + assert isinstance(result.samples, list) + for sample in result.samples: + assert type(sample.name) is str + assert result.entrez_id == entrez_id + assert result.hgnc_id == hgnc_id + assert type(result.description) is str or NoneType + assert type(result.gene_family) is str or NoneType + assert type(result.gene_function) is str or NoneType + assert type(result.immune_checkpoint) is str or NoneType + assert type(result.io_landscape_name) is str or NoneType + assert type(result.gene_pathway) is str or NoneType + assert type(result.super_category) is str or NoneType + assert type(result.therapy_type) is str or NoneType + assert repr(result) == '' % entrez_id + + +def test_Gene_with_publications(app, entrez_id, hgnc_id): + query = return_gene_query('publications') + result = query.filter_by(entrez_id=entrez_id).one_or_none() + + assert result + assert result.gene_set_assoc == [] + assert isinstance(result.publications, list) + for publication in result.publications[0:2]: + assert type(publication.pubmed_id) is int + assert result.entrez_id == entrez_id + assert result.hgnc_id == hgnc_id + assert repr(result) == '' % entrez_id + + +def test_Gene_with_copy_number_results(app, entrez_id): + query = return_gene_query('copy_number_results') + result = query.filter_by(entrez_id=entrez_id).one_or_none() + + assert result + assert isinstance(result.copy_number_results, list) + # Don't need to iterate through every result. + for copy_number_result in result.copy_number_results[0:2]: + assert copy_number_result.gene_id == result.id + + +def test_Gene_with_gene_sample_assoc(app, entrez_id): + query = return_gene_query('gene_sample_assoc') + result = query.filter_by(entrez_id=entrez_id).one_or_none() + + assert result + assert isinstance(result.gene_sample_assoc, list) + # Don't need to iterate through every result. + for gene_sample_rel in result.gene_sample_assoc[0:2]: + assert gene_sample_rel.gene_id == result.id + + +def test_Gene_with_gene_set_assoc(app, entrez_id): + query = return_gene_query('gene_set_assoc') + result = query.filter_by(entrez_id=entrez_id).one_or_none() + + assert result + assert isinstance(result.gene_set_assoc, list) + # Don't need to iterate through every result. + for gene_set_rel in result.gene_set_assoc[0:2]: + assert gene_set_rel.gene_id == result.id + + +def test_Gene_with_publication_gene_gene_set_assoc(app, entrez_id): + query = return_gene_query('publication_gene_gene_set_assoc') + result = query.filter_by(entrez_id=entrez_id).one_or_none() + + assert result + assert isinstance(result.publication_gene_gene_set_assoc, list) + # Don't need to iterate through every result. + for publication_gene_gene_set_rel in result.publication_gene_gene_set_assoc[0:2]: + assert publication_gene_gene_set_rel.gene_id == result.id + + +def test_Gene_no_relations(app, entrez_id, hgnc_id): + query = return_gene_query() + result = query.filter_by(entrez_id=entrez_id).one_or_none() + + assert result + assert result.copy_number_results == [] + assert result.gene_sample_assoc == [] + assert result.gene_sets == [] + assert result.samples == [] + assert result.entrez_id == entrez_id + assert result.hgnc_id == hgnc_id + assert type(result.description) is str or NoneType + assert type(result.gene_family) is str or NoneType + assert type(result.gene_function) is str or NoneType + assert type(result.immune_checkpoint) is str or NoneType + assert type(result.io_landscape_name) is str or NoneType + assert type(result.gene_pathway) is str or NoneType + assert type(result.super_category) is str or NoneType + assert type(result.therapy_type) is str or NoneType + + +def test_Gene_io_target(app): + + query = return_gene_query('gene_pathway', 'therapy_type') + result = query.filter_by(entrez_id=55).one_or_none() + + assert type(result.gene_pathway) is str + assert result.gene_pathway == 'Innate Immune System' + + assert type(result.therapy_type) is str + assert result.therapy_type == 'Targeted by Other Immuno-Oncology Therapy Type' diff --git a/apps/iatlas/api-gitlab/tests/db_models/test_GeneSet.py b/apps/iatlas/api-gitlab/tests/db_models/test_GeneSet.py new file mode 100644 index 0000000000..c8eeff6610 --- /dev/null +++ b/apps/iatlas/api-gitlab/tests/db_models/test_GeneSet.py @@ -0,0 +1,79 @@ +import pytest +from tests import NoneType +from api.database import return_gene_set_query + + +@pytest.fixture(scope='module') +def gene_set(): + return 'extracellular_network' + + +@pytest.fixture(scope='module') +def gene_set_1(): + return 'immunomodulator' + + +def test_gene_set_with_genes(app, gene_set): + query = return_gene_set_query('genes') + result = query.filter_by(name=gene_set).one_or_none() + + assert result + assert isinstance(result.genes, list) + assert len(result.genes) > 0 + # Don't need to iterate through every result. + for gene in result.genes[0:2]: + assert type(gene.entrez_id) is int + assert result.gene_set_assoc == [] + assert result.name == gene_set + assert type(result.display) is str or NoneType + assert repr(result) == '' % gene_set + + +def test_gene_set_with_gene_set_assoc(app, gene_set): + query = return_gene_set_query('gene_set_assoc') + result = query.filter_by(name=gene_set).one_or_none() + + assert result + assert isinstance(result.gene_set_assoc, list) + assert len(result.gene_set_assoc) > 0 + # Don't need to iterate through every result. + for gene_set_rel in result.gene_set_assoc[0:2]: + assert gene_set_rel.gene_set_id == result.id + + +def test_gene_set_with_publications(app, gene_set_1): + query = return_gene_set_query('publications') + result = query.filter_by(name=gene_set_1).one_or_none() + + assert result + assert isinstance(result.publications, list) + assert len(result.publications) > 0 + # Don't need to iterate through every result. + for publication in result.publications[0:2]: + assert type(publication.title) is str + + +def test_gene_set_with_publication_gene_gene_set_assoc(app, gene_set_1): + query = return_gene_set_query('publication_gene_gene_set_assoc') + result = query.filter_by(name=gene_set_1).one_or_none() + + assert result + assert isinstance(result.publication_gene_gene_set_assoc, list) + assert len(result.publication_gene_gene_set_assoc) > 0 + # Don't need to iterate through every result. + for publication_gene_gene_set_rel in result.publication_gene_gene_set_assoc[0:2]: + assert publication_gene_gene_set_rel.gene_set_id == result.id + + +def test_gene_set_no_relations(app, gene_set): + query = return_gene_set_query() + result = query.filter_by(name=gene_set).one_or_none() + + assert result + assert result.gene_set_assoc == [] + assert result.genes == [] + assert result.publications == [] + assert result.publication_gene_gene_set_assoc == [] + assert type(result.id) is str + assert result.name == gene_set + assert type(result.display) is str or NoneType diff --git a/apps/iatlas/api-gitlab/tests/db_models/test_GeneToGeneSet.py b/apps/iatlas/api-gitlab/tests/db_models/test_GeneToGeneSet.py new file mode 100644 index 0000000000..388fcd16e6 --- /dev/null +++ b/apps/iatlas/api-gitlab/tests/db_models/test_GeneToGeneSet.py @@ -0,0 +1,51 @@ +import pytest +from api.database import return_gene_to_gene_set_query + + +@pytest.fixture(scope='module') +def gt_entrez_id(test_db): + return 186 + + +@pytest.fixture(scope='module') +def gt_gene_id(test_db, gt_entrez_id): + from api.db_models import Gene + (id, ) = test_db.session.query(Gene.id).filter_by( + entrez_id=gt_entrez_id).one_or_none() + return id + + +def test_GeneToType_with_relations(app, gt_gene_id): + relationships_to_join = ['genes', 'types'] + + query = return_gene_to_gene_set_query(*relationships_to_join) + results = query.filter_by(gene_id=gt_gene_id).limit(3).all() + + assert isinstance(results, list) + for result in results: + assert result.gene_id == gt_gene_id + assert isinstance(result.genes, list) + # Don't need to iterate through every result. + for gene in result.genes[0:2]: + assert gene.id == gt_gene_id + assert type(gene.entrez_id) is int + assert isinstance(result.gene_sets, list) + # Don't need to iterate through every result. + for gene_set in result.gene_sets[0:2]: + assert type(gene_set.name) is str + assert type(result.gene_set_id) is str + assert repr(result) == '' % gt_gene_id + assert repr(results) == '[]' % gt_gene_id + + +def test_GeneToGeneSet_no_relations(app, gt_gene_id): + query = return_gene_to_gene_set_query() + results = query.filter_by(gene_id=gt_gene_id).limit(3).all() + + assert isinstance(results, list) + for result in results: + assert result.genes == [] + assert result.gene_sets == [] + assert result.gene_id == gt_gene_id + assert type(result.gene_set_id) is str + assert type(result.id) is str diff --git a/apps/iatlas/api-gitlab/tests/db_models/test_GeneToSample.py b/apps/iatlas/api-gitlab/tests/db_models/test_GeneToSample.py new file mode 100644 index 0000000000..59e95fe425 --- /dev/null +++ b/apps/iatlas/api-gitlab/tests/db_models/test_GeneToSample.py @@ -0,0 +1,90 @@ +import pytest +from tests import NoneType +from decimal import Decimal +from api.database import return_gene_to_sample_query + + +@pytest.fixture(scope='module') +def gs_entrez_id(test_db): + return 1 + + +@pytest.fixture(scope='module') +def gs_gene_id(test_db, gs_entrez_id): + from api.db_models import Gene + (id, ) = test_db.session.query(Gene.id).filter_by( + entrez_id=gs_entrez_id).one_or_none() + return id + + +@pytest.fixture(scope='module') +def nanostring_sample(): + return "Chen_CanDisc_2016-c08-ar-c08_pre" + +@pytest.fixture(scope='module') +def nanostring_entrez_id(): + return 933 + +@pytest.fixture(scope='module') +def nanostring_sample_id(test_db, nanostring_sample): + from api.db_models import Sample + (id, ) = test_db.session.query(Sample.id).filter_by( + name=nanostring_sample).one_or_none() + return id + +@pytest.fixture(scope='module') +def nanostring_gene_id(test_db, nanostring_entrez_id): + from api.db_models import Gene + (id, ) = test_db.session.query(Gene.id).filter_by( + entrez_id=nanostring_entrez_id).one_or_none() + return id + + + +def test_GeneToSample_with_relations(app, gs_entrez_id, gs_gene_id): + string_representation_list = [] + separator = ', ' + relationships_to_join = ['gene', 'sample'] + + query = return_gene_to_sample_query(*relationships_to_join) + results = query.filter_by(gene_id=gs_gene_id).limit(3).all() + assert isinstance(results, list) + + for result in results: + string_representation = '' % gs_gene_id + string_representation_list.append(string_representation) + assert result.gene.entrez_id == gs_entrez_id + assert type(result.sample.name) is str + assert result.gene_id == gs_gene_id + assert type(result.sample_id) is str + assert repr(result) == string_representation + assert repr(results) == '[' + separator.join( + string_representation_list) + ']' + + +def test_GeneToSample_no_relations(app, gs_gene_id): + query = return_gene_to_sample_query() + results = query.filter_by(gene_id=gs_gene_id).limit(3).all() + assert isinstance(results, list) + + for result in results: + assert type(result.gene) is NoneType + assert type(result.sample) is NoneType + assert result.gene_id == gs_gene_id + assert type(result.sample_id) is str + + +def test_GeneToSample_nanostring(app, nanostring_sample_id, nanostring_gene_id): + query = return_gene_to_sample_query() + results = query.filter_by(sample_id=nanostring_sample_id).filter_by(gene_id=nanostring_gene_id).all() + + assert isinstance(results, list) + assert len(results) == 1 + for result in results: + assert result.gene_id == nanostring_gene_id + assert result.sample_id == nanostring_sample_id + assert type(result.rna_seq_expression) is float or NoneType + assert type(result.nanostring_expression) is Decimal + + + diff --git a/apps/iatlas/api-gitlab/tests/db_models/test_GermlineGwasResult.py b/apps/iatlas/api-gitlab/tests/db_models/test_GermlineGwasResult.py new file mode 100644 index 0000000000..b099036005 --- /dev/null +++ b/apps/iatlas/api-gitlab/tests/db_models/test_GermlineGwasResult.py @@ -0,0 +1,97 @@ +import pytest +from tests import NoneType +from api.database import return_germline_gwas_result_query + + +@pytest.fixture(scope='module') +def ggr_feature(test_db): + return 'Eosinophils' + + +@pytest.fixture(scope='module') +def ggr_feature_id(test_db, ggr_feature): + from api.db_models import Feature + (id, ) = test_db.session.query(Feature.id).filter_by( + name=ggr_feature).one_or_none() + return id + + +@pytest.fixture(scope='module') +def ggr_snp(test_db): + return '13:105003803:C:T' + + +@pytest.fixture(scope='module') +def ggr_snp_id(test_db, ggr_snp): + from api.db_models import Snp + (id, ) = test_db.session.query(Snp.id).filter_by( + name=ggr_snp).one_or_none() + return id + + +def test_GermlineGwasResult_with_relations(app, data_set, data_set_id, ggr_feature, ggr_feature_id, ggr_snp, ggr_snp_id): + string_representation_list = [] + separator = ', ' + relationships_to_join = ['data_set', 'feature', 'snp'] + + query = return_germline_gwas_result_query(*relationships_to_join) + results = query.filter_by(dataset_id=data_set_id).filter_by( + feature_id=ggr_feature_id).filter_by( + snp_id=ggr_snp_id).limit(3).all() + + assert isinstance(results, list) + assert len(results) > 0 + for result in results: + germline_gwas_result_id = result.id + string_representation = '' % germline_gwas_result_id + string_representation_list.append(string_representation) + assert result.feature.id == ggr_feature_id + assert result.feature.name == ggr_feature + assert result.snp.id == ggr_snp_id + assert result.snp.name == ggr_snp + assert result.data_set.id == data_set_id + assert result.data_set.name == data_set + assert type(result.p_value) is float or NoneType + assert type(result.maf) is float or NoneType + assert repr(result) == string_representation + assert repr(results) == '[' + separator.join( + string_representation_list) + ']' + + +def test_GermlineGwasResult_no_relations(app, data_set_id, ggr_feature_id, ggr_snp_id): + query = return_germline_gwas_result_query() + results = query.filter_by(dataset_id=data_set_id).filter_by( + feature_id=ggr_feature_id).filter_by( + snp_id=ggr_snp_id).limit(3).all() + + assert isinstance(results, list) + assert len(results) > 0 + for result in results: + germline_gwas_result_id = result.id + string_representation = '' % germline_gwas_result_id + assert type(result.data_set) is NoneType + assert type(result.feature) is NoneType + assert type(result.snp) is NoneType + assert result.dataset_id == data_set_id + assert result.feature_id == ggr_feature_id + assert result.snp_id == ggr_snp_id + assert type(result.p_value) is float or NoneType + assert type(result.maf) is float or NoneType + assert repr(result) == string_representation + + +def test_GermlineGwasResult_no_filters(app): + query = return_germline_gwas_result_query() + results = query.limit(3).all() + + assert isinstance(results, list) + assert len(results) > 0 + for result in results: + germline_gwas_result_id = result.id + string_representation = '' % germline_gwas_result_id + assert type(result.data_set) is NoneType + assert type(result.feature) is NoneType + assert type(result.snp) is NoneType + assert type(result.p_value) is float or NoneType + assert type(result.maf) is float or NoneType + assert repr(result) == string_representation diff --git a/apps/iatlas/api-gitlab/tests/db_models/test_HeritabilityResult.py b/apps/iatlas/api-gitlab/tests/db_models/test_HeritabilityResult.py new file mode 100644 index 0000000000..9f112e57a1 --- /dev/null +++ b/apps/iatlas/api-gitlab/tests/db_models/test_HeritabilityResult.py @@ -0,0 +1,65 @@ +import pytest +from tests import NoneType +from api.database import return_heritability_result_query + + +@pytest.fixture(scope='module') +def hr_feature(test_db): + return 'BCR_Richness' + + +@pytest.fixture(scope='module') +def hr_feature_id(test_db, hr_feature): + from api.db_models import Feature + (id, ) = test_db.session.query(Feature.id).filter_by( + name=hr_feature).one_or_none() + return id + + +def test_HeritabilityResult_with_relations(app, data_set, data_set_id, hr_feature, hr_feature_id): + string_representation_list = [] + separator = ', ' + relationships_to_join = ['data_set', 'feature'] + + query = return_heritability_result_query(*relationships_to_join) + results = query.filter_by(dataset_id=data_set_id).filter_by( + feature_id=hr_feature_id).limit(3).all() + + assert isinstance(results, list) + assert len(results) > 0 + for result in results: + heritability_result_id = result.id + string_representation = '' % heritability_result_id + string_representation_list.append(string_representation) + assert result.data_set.id == data_set_id + assert result.data_set.name == data_set + assert result.feature.id == hr_feature_id + assert result.feature.name == hr_feature + assert type(result.p_value) is float or NoneType + assert type(result.variance) is float or NoneType + assert type(result.se) is float or NoneType + assert type(result.cluster) is str + assert repr(result) == string_representation + assert repr(results) == '[' + separator.join( + string_representation_list) + ']' + + +def test_HeritabilityResult_no_relations(app, data_set_id, hr_feature_id): + query = return_heritability_result_query() + results = query.filter_by(dataset_id=data_set_id).filter_by( + feature_id=hr_feature_id).limit(3).all() + + assert isinstance(results, list) + assert len(results) > 0 + for result in results: + heritability_result_id = result.id + string_representation = '' % heritability_result_id + assert type(result.data_set) is NoneType + assert type(result.feature) is NoneType + assert result.dataset_id == data_set_id + assert result.feature_id == hr_feature_id + assert type(result.p_value) is float or NoneType + assert type(result.variance) is float or NoneType + assert type(result.se) is float or NoneType + assert type(result.cluster) is str + assert repr(result) == string_representation diff --git a/apps/iatlas/api-gitlab/tests/db_models/test_Mutation.py b/apps/iatlas/api-gitlab/tests/db_models/test_Mutation.py new file mode 100644 index 0000000000..81e7284db9 --- /dev/null +++ b/apps/iatlas/api-gitlab/tests/db_models/test_Mutation.py @@ -0,0 +1,77 @@ +import pytest +from tests import NoneType +from api.database import return_mutation_query +from api.db_models import Mutation + + +@pytest.fixture(scope='module') +def mutation_entrez_id(test_db): + return 92 + + +@pytest.fixture(scope='module') +def mutation_gene_id(test_db, mutation_entrez_id): + from api.db_models import Gene + (id, ) = test_db.session.query(Gene.id).filter_by( + entrez_id=mutation_entrez_id).one_or_none() + return id + + +def test_Mutation_no_relations(app, mutation_gene_id): + query = return_mutation_query() + results = query.filter_by(gene_id=mutation_gene_id).limit(3).all() + + assert isinstance(results, list) + for result in results: + assert type(result.id) is str + assert type(result.name) is str + assert type(result.mutation_code) is str + assert type(result.mutation_type_id) is str + + assert type(result.gene) is NoneType + assert type(result.mutation_type) is NoneType + assert result.samples == [] + + assert result.gene_id == mutation_gene_id + assert type(result.gene_id) is str + + +def test_Mutation_with_relations(app, mutation_entrez_id, mutation_gene_id): + string_representation_list = [] + separator = ', ' + relationships_to_load = [ + 'gene', 'mutation_code', 'mutation_type', 'samples'] + + query = return_mutation_query(*relationships_to_load) + results = query.filter_by(gene_id=mutation_gene_id).limit(3).all() + + assert isinstance(results, list) + assert len(results) > 0 + for result in results: + mutation_id = result.id + string_representation = '' % mutation_id + string_representation_list.append(string_representation) + assert type(result.name) is str + assert result.gene.entrez_id == mutation_entrez_id + assert result.gene.id == mutation_gene_id + if result.samples: + assert isinstance(result.samples, list) + for sample in result.samples[0:2]: + assert type(sample.id) is str + assert result.gene_id == mutation_gene_id + assert type(result.gene_id) is str + assert type(result.mutation_code) is str + assert type(result.mutation_type_id) is str + assert repr(result) == string_representation + assert repr(results) == '[' + separator.join( + string_representation_list) + ']' + + +def test_Mutation_with_sample_mutation_assoc(app, mutation_gene_id): + query = return_mutation_query('sample_mutation_assoc') + result = query.filter_by(gene_id=mutation_gene_id).first() + + if result.sample_mutation_assoc: + assert isinstance(result.sample_mutation_assoc, list) + for sample_mutation_rel in result.sample_mutation_assoc[0:2]: + assert sample_mutation_rel.mutation_id == result.id diff --git a/apps/iatlas/api-gitlab/tests/db_models/test_MutationType.py b/apps/iatlas/api-gitlab/tests/db_models/test_MutationType.py new file mode 100644 index 0000000000..9540a048fa --- /dev/null +++ b/apps/iatlas/api-gitlab/tests/db_models/test_MutationType.py @@ -0,0 +1,35 @@ +import pytest +from tests import NoneType +from api.database import return_mutation_type_query +from api.db_models import MutationType + + +@pytest.fixture(scope='module') +def mutation_type_name(): + return 'driver_mutation' + + +def test_MutationType_with_relations(app, mutation_type_name): + query = return_mutation_type_query('mutations') + result = query.filter_by(name=mutation_type_name).first() + + if result.mutations: + assert isinstance(result.mutations, list) + # Don't need to iterate through every result. + for mutation in result.mutations[0:2]: + assert type(mutation.id) is str + assert type(mutation.name) is str + assert type(mutation.mutation_code) is str + + assert result.name == mutation_type_name + assert type(result.display) is str + assert repr(result) == '' % mutation_type_name + + +def test_MutationType_no_relations(app, mutation_type_name): + query = return_mutation_type_query() + result = query.filter_by(name=mutation_type_name).first() + assert result.mutations == [] + assert type(result.id) is str + assert result.name == mutation_type_name + assert type(result.display) is str or NoneType diff --git a/apps/iatlas/api-gitlab/tests/db_models/test_Neoantigen.py b/apps/iatlas/api-gitlab/tests/db_models/test_Neoantigen.py new file mode 100644 index 0000000000..fd48f7d0fd --- /dev/null +++ b/apps/iatlas/api-gitlab/tests/db_models/test_Neoantigen.py @@ -0,0 +1,70 @@ +import pytest +from tests import NoneType +from api.database import return_neoantigen_query +from api.db_models import Neoantigen + + +@pytest.fixture(scope='module') +def test_neoantigen(test_db): + query = test_db.session.query( + Neoantigen.id, + Neoantigen.patient_id, + Neoantigen.neoantigen_gene_id, + Neoantigen.pmhc, + Neoantigen.freq_pmhc, + Neoantigen.tpm + ) + return query.limit(1).one_or_none() + + +def test_Neoantigen_no_relations(app): + query = return_neoantigen_query() + results = query.limit(10).all() + assert isinstance(results, list) + assert len(results) == 10 + for result in results: + assert type(result.id) is str + assert type(result.tpm) is float or NoneType + assert type(result.pmhc) is str + assert type(result.freq_pmhc) is int + assert type(result.patient_id) is str + assert type(result.neoantigen_gene_id) is str or NoneType + assert result.patient == [] + assert result.gene == [] + string_representation = '' % result.id + assert repr(result) == string_representation + + +def test_Neoantigen_with_relations(app): + query = return_neoantigen_query("patient", "gene") + results = query.limit(10).all() + assert isinstance(results, list) + assert len(results) == 10 + for result in results: + assert type(result.id) is str + assert type(result.tpm) is float or NoneType + assert type(result.pmhc) is str + assert type(result.freq_pmhc) is int + assert type(result.patient_id) is str + assert type(result.neoantigen_gene_id) is str or NoneType + assert result.patient[0].id == result.patient_id + assert result.gene[0].id == result.neoantigen_gene_id + string_representation = '' % result.id + assert repr(result) == string_representation + + +def test_specific_Neoantigen(app, test_neoantigen): + query = return_neoantigen_query() + query = query.filter_by(patient_id=test_neoantigen.patient_id) + query = query.filter_by( + neoantigen_gene_id=test_neoantigen.neoantigen_gene_id) + query = query.filter_by(pmhc=test_neoantigen.pmhc) + result = query.one_or_none() + assert result.id == test_neoantigen.id + #assert result.tpm == test_neoantigen.tpm + assert result.pmhc == test_neoantigen.pmhc + assert result.freq_pmhc == test_neoantigen.freq_pmhc + assert result.patient_id == test_neoantigen.patient_id + assert result.neoantigen_gene_id == test_neoantigen.neoantigen_gene_id + assert result.patient == [] + assert result.gene == [] diff --git a/apps/iatlas/api-gitlab/tests/db_models/test_Node.py b/apps/iatlas/api-gitlab/tests/db_models/test_Node.py new file mode 100644 index 0000000000..03af3235fd --- /dev/null +++ b/apps/iatlas/api-gitlab/tests/db_models/test_Node.py @@ -0,0 +1,106 @@ +import pytest +from tests import NoneType +from api.database import return_node_query + + +@pytest.fixture(scope='module') +def node_entrez_id(test_db): + return 5817 + + +@pytest.fixture(scope='module') +def node_gene_id(test_db, node_entrez_id): + from api.db_models import Gene + (id, ) = test_db.session.query(Gene.id).filter_by( + entrez_id=node_entrez_id).one_or_none() + return id + + +def test_Node_with_relations(app, node_entrez_id, node_gene_id): + string_representation_list = [] + separator = ', ' + + relationships_to_load = ['data_set', 'gene', 'feature'] + query = return_node_query(*relationships_to_load) + results = query.filter_by(node_gene_id=node_gene_id).limit(3).all() + + assert isinstance(results, list) + for result in results: + node_id = result.id + string_representation = '' % node_id + string_representation_list.append(string_representation) + if result.data_set: + assert result.data_set.id == result.dataset_id + assert result.gene.entrez_id == node_entrez_id + if result.feature: + assert result.node_feature.id == result.node_feature_id + assert result.edges_primary == [] + assert result.edges_secondary == [] + assert type(result.tag1) is NoneType + assert result.node_gene_id == node_gene_id + assert type(result.node_feature_id) is NoneType + assert type(result.name) is str + assert type(result.network) is str + assert type(result.label) is str or NoneType + assert type(result.score) is float or NoneType + assert type(result.x) is float or NoneType + assert type(result.y) is float or NoneType + assert repr(result) == string_representation + assert repr(results) == '[' + separator.join( + string_representation_list) + ']' + + +def test_Node_with_edges_primary(app, node_gene_id): + query = return_node_query('edges_primary') + result = query.filter_by(node_gene_id=node_gene_id).first() + + if result.edges_primary: + assert isinstance(result.edges_primary, list) + # Don't need to iterate through every result. + for edge_primary in result.edges_primary[0:2]: + assert edge_primary.node_1_id == result.id + + +def test_Node_with_edges_secondary(app, node_gene_id): + query = return_node_query('edges_secondary') + result = query.filter_by(node_gene_id=node_gene_id).first() + + if result.edges_secondary: + assert isinstance(result.edges_secondary, list) + # Don't need to iterate through every result. + for edge_secondary in result.edges_secondary[0:2]: + assert edge_secondary.node_2_id == result.id + + +def test_Node_with_tags(app, node_gene_id): + query = return_node_query('tag1', 'tag2') + result = query.filter_by(name="TCGA_extracellular_network_C1:ACC_2").first() + + tag1 = result.tag1 + assert tag1.name == 'C1' + + tag2 = result.tag2 + assert tag2.name == 'ACC' + + +def test_Node_no_relations(app, node_gene_id): + query = return_node_query() + results = query.filter_by(node_gene_id=node_gene_id).limit(3).all() + + assert isinstance(results, list) + for result in results: + assert type(result.gene) is NoneType + assert type(result.feature) is NoneType + assert result.edges_primary == [] + assert result.edges_secondary == [] + assert type(result.tag1) is NoneType + assert type(result.id) is str + assert type(result.dataset_id) is str + assert result.node_gene_id == node_gene_id + assert type(result.network) is str + assert type(result.node_feature_id) is NoneType + assert type(result.name) is str + assert type(result.label) is str or NoneType + assert type(result.score) is float or NoneType + assert type(result.x) is float or NoneType + assert type(result.y) is float or NoneType diff --git a/apps/iatlas/api-gitlab/tests/db_models/test_Patient.py b/apps/iatlas/api-gitlab/tests/db_models/test_Patient.py new file mode 100644 index 0000000000..d925db36a6 --- /dev/null +++ b/apps/iatlas/api-gitlab/tests/db_models/test_Patient.py @@ -0,0 +1,52 @@ +import pytest +from tests import NoneType +from api.database import return_patient_query +from api.enums import ethnicity_enum, gender_enum, race_enum + + +@pytest.fixture(scope='module') +def name(): + return 'TCGA-WN-AB4C' + + +def test_Patient_with_relations(app, name): + relationships_to_load = ['samples', 'slides'] + + query = return_patient_query(*relationships_to_load) + result = query.filter_by(name=name).one_or_none() + + if result.samples: + assert isinstance(result.samples, list) + assert len(result.samples) > 0 + # Don't need to iterate through every result. + for sample in result.samples[0:2]: + assert type(sample.name) is str + if result.slides: + assert isinstance(result.slides, list) + assert len(result.slides) > 0 + # Don't need to iterate through every result. + for slide in result.slides[0:2]: + assert type(slide.name) is str + assert result.name == name + assert type(result.age_at_diagnosis) is int or NoneType + assert type(result.ethnicity) in ethnicity_enum.enums or NoneType + assert type(result.gender) in gender_enum.enums or NoneType + assert type(result.height) is int or NoneType + assert type(result.race) in race_enum.enums or NoneType + assert type(result.weight) is int or NoneType + assert repr(result) == '' % name + + +def test_Patient_no_relations(app, name): + query = return_patient_query() + result = query.filter_by(name=name).one_or_none() + + assert result.samples == [] + assert result.slides == [] + assert result.name == name + assert type(result.age_at_diagnosis) is int or NoneType + assert type(result.ethnicity) in ethnicity_enum.enums or NoneType + assert type(result.gender) in gender_enum.enums or NoneType + assert type(result.height) is int or NoneType + assert type(result.race) in race_enum.enums or NoneType + assert type(result.weight) is int or NoneType diff --git a/apps/iatlas/api-gitlab/tests/db_models/test_Publication.py b/apps/iatlas/api-gitlab/tests/db_models/test_Publication.py new file mode 100644 index 0000000000..22d64ff45f --- /dev/null +++ b/apps/iatlas/api-gitlab/tests/db_models/test_Publication.py @@ -0,0 +1,104 @@ +import pytest +from tests import NoneType +from api.database import return_publication_query + + +@pytest.fixture(scope='module') +def publication_title_with_tag(): + return 'Comprehensive Pan-Genomic Characterization of Adrenocortical Carcinoma.' + +@pytest.fixture(scope='module') +def publication_title_with_gene(): + return 'Immunotherapy: Beyond Anti-PD-1 and Anti-PD-L1 Therapies.' + +def test_publication_with_genes(app, publication_title_with_gene): + query = return_publication_query('genes') + result = query.filter_by(title=publication_title_with_gene).one_or_none() + + assert result + assert isinstance(result.genes, list) + assert len(result.genes) > 0 + # Don't need to iterate through every result. + for gene in result.genes[0:2]: + assert type(gene.entrez_id) is int + assert result.publication_gene_gene_set_assoc == [] + assert result.title == publication_title_with_gene + assert type(result.do_id) is str or NoneType + assert type(result.first_author_last_name) is str or NoneType + assert type(result.journal) is str or NoneType + assert type(result.pubmed_id) is int or NoneType + assert type(result.year) is int or NoneType + assert repr(result) == '' % publication_title_with_gene + + +def test_with_gene_sets(app, publication_title_with_gene): + query = return_publication_query('gene_sets') + result = query.filter_by(title=publication_title_with_gene).one_or_none() + + assert result + assert isinstance(result.gene_sets, list) + assert len(result.gene_sets) > 0 + # Don't need to iterate through every result. + for gene_set in result.gene_sets[0:2]: + assert type(gene_set.name) is str + + +def test_publication_with_publication_gene_gene_set_assoc(app, publication_title_with_gene): + query = return_publication_query('publication_gene_gene_set_assoc') + result = query.filter_by(title=publication_title_with_gene).one_or_none() + + assert result + assert isinstance(result.publication_gene_gene_set_assoc, list) + assert len(result.publication_gene_gene_set_assoc) > 0 + # Don't need to iterate through every result. + for publication_gene_gene_set_rel in result.publication_gene_gene_set_assoc[0:2]: + assert publication_gene_gene_set_rel.publication_id == result.id + + +def test_publication_with_tag_publication_assoc(app, publication_title_with_tag): + query = return_publication_query('tag_publication_assoc') + result = query.filter_by(title=publication_title_with_tag).one_or_none() + + assert result + assert isinstance(result.tag_publication_assoc, list) + assert len(result.tag_publication_assoc) > 0 + # Don't need to iterate through every result. + for tag_publication_rel in result.tag_publication_assoc[0:2]: + assert tag_publication_rel.publication_id == result.id + + +def test_publication_with_tags(app, publication_title_with_tag): + query = return_publication_query('tags') + result = query.filter_by(title=publication_title_with_tag).one_or_none() + + assert result + assert isinstance(result.tags, list) + assert len(result.tags) > 0 + # Don't need to iterate through every result. + for tag in result.tags[0:5]: + assert type(tag.name) is str + assert result.genes == [] + assert result.gene_sets == [] + assert result.publication_gene_gene_set_assoc == [] + assert result.title == publication_title_with_tag + assert type(result.do_id) is str or NoneType + assert type(result.first_author_last_name) is str or NoneType + assert type(result.journal) is str or NoneType + assert type(result.pubmed_id) is int or NoneType + assert type(result.year) is int or NoneType + assert repr(result) == '' % publication_title_with_tag + + +def test_publication_no_relations(app, publication_title_with_tag): + query = return_publication_query() + result = query.filter_by(title=publication_title_with_tag).one_or_none() + + assert result + assert result.genes == [] + assert result.publication_gene_gene_set_assoc == [] + assert result.title == publication_title_with_tag + assert type(result.do_id) is str or NoneType + assert type(result.first_author_last_name) is str or NoneType + assert type(result.journal) is str or NoneType + assert type(result.pubmed_id) is int or NoneType + assert type(result.year) is int or NoneType diff --git a/apps/iatlas/api-gitlab/tests/db_models/test_PublicationToGeneToGeneSet.py b/apps/iatlas/api-gitlab/tests/db_models/test_PublicationToGeneToGeneSet.py new file mode 100644 index 0000000000..08e2a75fe0 --- /dev/null +++ b/apps/iatlas/api-gitlab/tests/db_models/test_PublicationToGeneToGeneSet.py @@ -0,0 +1,87 @@ +import pytest +from api.database import return_publication_to_gene_to_gene_set_query + + +@pytest.fixture(scope='module') +def pggt_entrez_id(test_db): + return 958 + + +@pytest.fixture(scope='module') +def pggt_gene_id(test_db, pggt_entrez_id): + from api.db_models import Gene + (id, ) = test_db.session.query(Gene.id).filter_by( + entrez_id=pggt_entrez_id).one_or_none() + return id + + +def test_PublicationToGeneToGeneSet_with_genes(app, pggt_entrez_id, pggt_gene_id): + string_representation_list = [] + separator = ', ' + + query = return_publication_to_gene_to_gene_set_query('genes') + results = query.filter_by(gene_id=pggt_gene_id).limit(3).all() + + assert isinstance(results, list) + assert len(results) > 0 + for result in results: + string_representation = '' % pggt_gene_id + string_representation_list.append(string_representation) + assert isinstance(result.genes, list) + assert len(result.genes) > 0 + # Don't need to iterate through every result. + for gene in result.genes[0:2]: + assert gene.entrez_id == pggt_entrez_id + assert gene.id == pggt_gene_id + assert result.gene_id == pggt_gene_id + assert type(result.publication_id) is str + assert repr(result) == string_representation + assert repr(results) == '[' + separator.join( + string_representation_list) + ']' + + +def test_PublicationToGeneToGeneSet_with_publications(app, pggt_gene_id): + query = return_publication_to_gene_to_gene_set_query('publications') + results = query.filter_by(gene_id=pggt_gene_id).limit(3).all() + + assert isinstance(results, list) + assert len(results) > 0 + for result in results: + assert isinstance(result.publications, list) + assert len(result.publications) > 0 + # Don't need to iterate through every result. + for publication in result.publications[0:2]: + assert type(publication.pubmed_id) is int + assert result.gene_id == pggt_gene_id + assert type(result.publication_id) is str + + +def test_PublicationToGeneToGeneSet_with_gene_types(app, pggt_gene_id): + query = return_publication_to_gene_to_gene_set_query('gene_sets') + results = query.filter_by(gene_id=pggt_gene_id).limit(3).all() + + assert isinstance(results, list) + assert len(results) > 0 + for result in results: + assert isinstance(result.gene_sets, list) + assert len(result.gene_sets) > 0 + # Don't need to iterate through every result. + for gene_set in result.gene_sets[0:2]: + assert type(gene_set.name) is str + assert result.gene_id == pggt_gene_id + assert type(result.gene_set_id) is str + + +def test_PublicationToGeneToGeneSet_no_relations(app, pggt_gene_id): + query = return_publication_to_gene_to_gene_set_query() + results = query.filter_by(gene_id=pggt_gene_id).limit(3).all() + + assert isinstance(results, list) + assert len(results) > 0 + for result in results: + assert result.genes == [] + assert result.publications == [] + assert result.gene_sets == [] + assert result.gene_id == pggt_gene_id + assert type(result.publication_id) is str + assert type(result.gene_set_id) is str diff --git a/apps/iatlas/api-gitlab/tests/db_models/test_RareVariantPathwayAssociations.py b/apps/iatlas/api-gitlab/tests/db_models/test_RareVariantPathwayAssociations.py new file mode 100644 index 0000000000..02d1288423 --- /dev/null +++ b/apps/iatlas/api-gitlab/tests/db_models/test_RareVariantPathwayAssociations.py @@ -0,0 +1,79 @@ +import pytest +from tests import NoneType +from decimal import Decimal +from api.database import return_rare_variant_pathway_associations_query + + +@pytest.fixture(scope='module') +def rvap_pathway(test_db): + return 'MMR' + + +@pytest.fixture(scope='module') +def rvap_feature(test_db): + return 'BCR_Richness' + + +@pytest.fixture(scope='module') +def rvap_feature_id(test_db, rvap_feature): + from api.db_models import Feature + (id, ) = test_db.session.query(Feature.id).filter_by( + name=rvap_feature).one_or_none() + return id + + +def test_RareVariantPathwayAssociation_with_relations(app, data_set, data_set_id, rvap_feature, rvap_feature_id, rvap_pathway): + string_representation_list = [] + separator = ', ' + relationships_to_join = ['data_set', 'feature'] + + query = return_rare_variant_pathway_associations_query(*relationships_to_join) + results = query.filter_by(dataset_id=data_set_id).filter_by(feature_id=rvap_feature_id).filter_by(pathway=rvap_pathway).limit(3).all() + + assert isinstance(results, list) + assert len(results) == 1 + for result in results: + rare_variant_pathway_association_id = result.id + string_representation = '' % rare_variant_pathway_association_id + string_representation_list.append(string_representation) + assert result.data_set.id == data_set_id + assert result.data_set.name == data_set + assert result.feature.id == rvap_feature_id + assert result.feature.name == rvap_feature + assert type(result.pathway) is str + assert type(result.p_value) is Decimal + assert type(result.min) is Decimal + assert type(result.max) is Decimal + assert type(result.mean) is Decimal + assert type(result.q1) is Decimal + assert type(result.q2) is Decimal + assert type(result.q3) is Decimal + assert type(result.n_mutants) is int + assert type(result.n_total) is int + assert repr(result) == string_representation + assert repr(results) == '[' + \ + separator.join(string_representation_list) + ']' + + +def test_RareVariantPathwayAssociation_no_relations(app, data_set_id, rvap_feature_id, rvap_pathway): + query = return_rare_variant_pathway_associations_query() + results = query.filter_by(dataset_id=data_set_id).filter_by( + feature_id=rvap_feature_id).filter_by(pathway=rvap_pathway).limit(3).all() + + assert isinstance(results, list) + assert len(results) > 0 + for result in results: + assert type(result.data_set) is NoneType + assert type(result.feature) is NoneType + assert result.dataset_id == data_set_id + assert result.feature_id == rvap_feature_id + assert type(result.pathway) is str + assert type(result.p_value) is Decimal or NoneType + assert type(result.min) is Decimal or NoneType + assert type(result.max) is Decimal or NoneType + assert type(result.mean) is Decimal or NoneType + assert type(result.q1) is Decimal or NoneType + assert type(result.q2) is Decimal or NoneType + assert type(result.q3) is Decimal or NoneType + assert type(result.n_mutants) is int or NoneType + assert type(result.n_total) is int or NoneType diff --git a/apps/iatlas/api-gitlab/tests/db_models/test_Sample.py b/apps/iatlas/api-gitlab/tests/db_models/test_Sample.py new file mode 100644 index 0000000000..b0e386ceb0 --- /dev/null +++ b/apps/iatlas/api-gitlab/tests/db_models/test_Sample.py @@ -0,0 +1,155 @@ +import pytest +from tests import NoneType +from api.database import return_sample_query + + +def test_Sample_with_data_sets(app, sample): + query = return_sample_query('data_sets') + result = query.filter_by(name=sample).one_or_none() + + assert result + assert result.name == sample + assert isinstance(result.data_sets, list) + assert len(result.data_sets) > 0 + # Don't need to iterate through every result. + for data_set in result.data_sets[0:2]: + assert type(data_set.name) is str + + +def test_Sample_with_dataset_sample_assoc(app, sample): + query = return_sample_query('dataset_sample_assoc') + result = query.filter_by(name=sample).one_or_none() + + assert result + assert result.name == sample + assert isinstance(result.dataset_sample_assoc, list) + assert len(result.dataset_sample_assoc) > 0 + # Don't need to iterate through every result. + for dataset_sample_rel in result.dataset_sample_assoc[0:2]: + assert dataset_sample_rel.sample_id == result.id + + +def test_Sample_with_feature_sample_assoc(app, sample): + query = return_sample_query('feature_sample_assoc') + result = query.filter_by(name=sample).one_or_none() + + assert result + assert result.name == sample + assert isinstance(result.feature_sample_assoc, list) + assert len(result.feature_sample_assoc) > 0 + # Don't need to iterate through every result. + for feature_sample_rel in result.feature_sample_assoc[0:2]: + assert feature_sample_rel.sample_id == result.id + + +def test_Sample_with_feature(app, sample): + query = return_sample_query('features') + result = query.filter_by(name=sample).one_or_none() + + assert result + assert result.name == sample + assert isinstance(result.features, list) + assert len(result.features) > 0 + # Don't need to iterate through every result. + for feature in result.features[0:2]: + assert type(feature.name) is str + + +def test_Sample_with_genes(app, sample): + query = return_sample_query('genes') + result = query.filter_by(name=sample).one_or_none() + + assert result + assert result.name == sample + assert isinstance(result.genes, list) + assert len(result.genes) > 0 + # Don't need to iterate through every result. + for gene in result.genes[0:2]: + assert type(gene.entrez_id) is int + + +def test_Sample_with_gene_sample_assoc(app, sample): + query = return_sample_query('gene_sample_assoc') + result = query.filter_by(name=sample).one_or_none() + + assert result + assert result.name == sample + assert isinstance(result.gene_sample_assoc, list) + assert len(result.gene_sample_assoc) > 0 + # Don't need to iterate through every result. + for gene_sample_rel in result.gene_sample_assoc[0:2]: + assert gene_sample_rel.sample_id == result.id + + +def test_Sample_with_mutations(app, sample): + query = return_sample_query('mutations') + result = query.filter_by(name=sample).one_or_none() + + assert result + assert result.name == sample + assert isinstance(result.mutations, list) + assert len(result.mutations) > 0 + # Don't need to iterate through every result. + for mutation in result.mutations[0:2]: + assert type(mutation.id) is str + + +def test_Sample_with_patient(app, sample): + query = return_sample_query('patient') + result = query.filter_by(name=sample).one_or_none() + + assert result + assert result.name == sample + assert result.patient.id == result.patient_id + + +def test_Sample_with_sample_mutation_assoc(app, sample): + query = return_sample_query('sample_mutation_assoc') + result = query.filter_by(name=sample).one_or_none() + + assert result + assert result.name == sample + assert isinstance(result.sample_mutation_assoc, list) + assert len(result.sample_mutation_assoc) > 0 + # Don't need to iterate through every result. + for sample_mutation_rel in result.sample_mutation_assoc[0:2]: + assert sample_mutation_rel.sample_id == result.id + + +def test_Sample_with_tags(app, sample): + query = return_sample_query('tags') + result = query.filter_by(name=sample).one_or_none() + + assert result + assert isinstance(result.tags, list) + assert len(result.tags) > 0 + # Don't need to iterate through every result. + for tag in result.tags[0:2]: + assert type(tag.name) is str + assert result.dataset_sample_assoc == [] + assert result.gene_sample_assoc == [] + assert result.feature_sample_assoc == [] + assert result.sample_mutation_assoc == [] + assert result.name == sample + assert type(result.patient_id) is int or NoneType + assert repr(result) == '' % sample + + +def test_Sample_no_relations(app, sample): + query = return_sample_query() + result = query.filter_by(name=sample).one_or_none() + + assert result + assert result.data_sets == [] + assert result.dataset_sample_assoc == [] + assert result.gene_sample_assoc == [] + assert result.feature_sample_assoc == [] + assert result.sample_mutation_assoc == [] + assert result.features == [] + assert result.genes == [] + assert result.mutations == [] + assert type(result.patient) is NoneType + assert result.tags == [] + assert type(result.id) is str + assert result.name == sample + assert type(result.patient_id) is str or NoneType diff --git a/apps/iatlas/api-gitlab/tests/db_models/test_SampleToMutation.py b/apps/iatlas/api-gitlab/tests/db_models/test_SampleToMutation.py new file mode 100644 index 0000000000..1005a6fa69 --- /dev/null +++ b/apps/iatlas/api-gitlab/tests/db_models/test_SampleToMutation.py @@ -0,0 +1,48 @@ +import pytest +from api.database import return_sample_to_mutation_query +from api.db_models import SampleToMutation +from api.enums import status_enum + + +def test_SampleToMutation_with_relations(app, sample_id): + string_representation_list = [] + separator = ', ' + + query = return_sample_to_mutation_query('samples', 'mutations') + results = query.filter_by(sample_id=sample_id).limit(3).all() + + assert isinstance(results, list) + assert len(results) > 0 + for result in results: + string_representation = '' % sample_id + string_representation_list.append(string_representation) + assert isinstance(result.mutations, list) + assert len(result.mutations) > 0 + # Don't need to iterate through every result. + for mutation in result.mutations[0:2]: + assert type(mutation.id) is str + assert isinstance(result.samples, list) + assert len(result.samples) > 0 + # Don't need to iterate through every result. + for sample in result.samples[0:2]: + assert sample.id == sample_id + assert result.sample_id == sample_id + assert type(result.mutation_id) is str + assert result.mutation_status in status_enum.enums + assert repr(result) == string_representation + assert repr(results) == '[' + separator.join( + string_representation_list) + ']' + + +def test_SampleToMutation_no_relations(app, sample_id): + query = return_sample_to_mutation_query() + results = query.filter_by(sample_id=sample_id).limit(3).all() + + assert isinstance(results, list) + assert len(results) > 0 + for result in results: + assert result.mutations == [] + assert result.samples == [] + assert result.sample_id == sample_id + assert type(result.mutation_id) is str + assert result.mutation_status in status_enum.enums diff --git a/apps/iatlas/api-gitlab/tests/db_models/test_SampleToTag.py b/apps/iatlas/api-gitlab/tests/db_models/test_SampleToTag.py new file mode 100644 index 0000000000..546129acb0 --- /dev/null +++ b/apps/iatlas/api-gitlab/tests/db_models/test_SampleToTag.py @@ -0,0 +1,44 @@ +import pytest +from api.database import return_sample_to_tag_query + + +def test_SampleToTag_with_relations(app, sample_id): + string_representation_list = [] + separator = ', ' + + query = return_sample_to_tag_query('samples', 'tags') + results = query.filter_by(sample_id=sample_id).limit(3).all() + + assert isinstance(results, list) + assert len(results) > 0 + for result in results: + string_representation = '' % sample_id + string_representation_list.append(string_representation) + assert isinstance(result.samples, list) + assert len(result.samples) > 0 + # Don't need to iterate through every result. + for sample in result.samples[0:2]: + assert sample.id == sample_id + assert isinstance(result.tags, list) + assert len(result.tags) > 0 + # Don't need to iterate through every result. + for tag in result.tags[0:2]: + assert type(tag.name) is str + assert result.sample_id == sample_id + assert type(result.tag_id) is str + assert repr(result) == string_representation + assert repr(results) == '[' + separator.join( + string_representation_list) + ']' + + +def test_SampleToTag_no_relations(app, sample_id): + query = return_sample_to_tag_query() + results = query.filter_by(sample_id=sample_id).limit(3).all() + + assert isinstance(results, list) + assert len(results) > 0 + for result in results: + assert result.samples == [] + assert result.tags == [] + assert result.sample_id == sample_id + assert type(result.tag_id) is str diff --git a/apps/iatlas/api-gitlab/tests/db_models/test_SingleCellPseudobulk.py b/apps/iatlas/api-gitlab/tests/db_models/test_SingleCellPseudobulk.py new file mode 100644 index 0000000000..ef098d6d72 --- /dev/null +++ b/apps/iatlas/api-gitlab/tests/db_models/test_SingleCellPseudobulk.py @@ -0,0 +1,23 @@ +import pytest +from api.database import return_single_cell_pseudobulk_query + +def test_query_with_no_relations(): + query = return_single_cell_pseudobulk_query() + results = query.limit(3).all() + assert isinstance(results, list) + for result in results: + assert isinstance(result.id, str) + assert isinstance(result.cell_type, str) + assert isinstance(result.gene_id, str) + assert isinstance(result.sample_id, str) + assert result.single_cell_seq_sum is not None + assert repr(result).startswith("' % slide + + +def test_Slide_no_relations(app, slide): + query = return_slide_query() + result = query.filter_by(name=slide).one_or_none() + + assert result + assert type(result.patient) is NoneType + assert result.name == slide + assert type(result.description) is str or NoneType + assert type(result.patient_id) is str diff --git a/apps/iatlas/api-gitlab/tests/db_models/test_Snp.py b/apps/iatlas/api-gitlab/tests/db_models/test_Snp.py new file mode 100644 index 0000000000..bce1a34e5c --- /dev/null +++ b/apps/iatlas/api-gitlab/tests/db_models/test_Snp.py @@ -0,0 +1,38 @@ +import pytest +from tests import NoneType +from api.database import return_snp_query + + +@pytest.fixture(scope='module') +def snp_name(): + return '7:104003135:C:G' + + +@pytest.fixture(scope='module') +def snp_rsid(): + return 'rs2188491' + + +@pytest.fixture(scope='module') +def snp_chr(): + return '7' + + +def test_snp(app, snp_name, snp_rsid, snp_chr): + query = return_snp_query('snps') + results = query.filter_by(name=snp_name).filter_by( + rsid=snp_rsid).filter_by(chr=snp_chr).limit(3).all() + assert isinstance(results, list) + assert len(results) > 0 + string_representation_list = [] + separator = ', ' + for result in results: + string_representation = '' % result.name + string_representation_list.append(string_representation) + assert type(result.name) is str + assert type(result.rsid) is str or NoneType + assert type(result.chr) is str or NoneType + assert type(result.bp) is int or NoneType + assert repr(result) == string_representation + assert repr(results) == '[' + separator.join( + string_representation_list) + ']' diff --git a/apps/iatlas/api-gitlab/tests/db_models/test_Tag.py b/apps/iatlas/api-gitlab/tests/db_models/test_Tag.py new file mode 100644 index 0000000000..aa7846fe6a --- /dev/null +++ b/apps/iatlas/api-gitlab/tests/db_models/test_Tag.py @@ -0,0 +1,168 @@ +import pytest +from tests import NoneType +from api.database import return_tag_query + + +@pytest.fixture(scope='module') +def tag_name(): + return 'ACC' + +@pytest.fixture(scope='module') +def tag_name_with_copy_number_results(): + return 'C1' + + +@pytest.fixture(scope='module') +def tag_with_publication(): + return 'AML_1' + + +@pytest.fixture(scope='module') +def tag_with_order(): + return 'actla4_prior_ici_rx' + + +@pytest.fixture(scope='module') +def tag_order(): + return 1 + + +def test_Tag_no_relations(app, tag_name): + query = return_tag_query() + result = query.filter_by(name=tag_name).one_or_none() + + assert result + assert result.related_tags == [] + assert result.samples == [] + assert result.tags == [] + assert result.copy_number_results == [] + assert result.driver_results == [] + assert type(result.id) is str + assert result.name == tag_name + assert type(result.description) is str + assert type(result.color) is str or NoneType + assert type(result.long_display) is str or NoneType + assert type(result.short_display) is str or NoneType + assert result.tag_type == 'group' + assert type(result.order) is int or NoneType + + +def test_Tag_with_order(app, tag_with_order, tag_order): + query = return_tag_query() + result = query.filter_by(name=tag_with_order).one_or_none() + assert result.name == tag_with_order + assert result.tag_type == 'group' + assert result.order == tag_order + + +def test_Tag_with_copy_number_results(app, tag_name_with_copy_number_results): + query = return_tag_query('copy_number_results') + result = query.filter_by(name=tag_name_with_copy_number_results).one_or_none() + + assert result + assert isinstance(result.copy_number_results, list) + assert len(result.copy_number_results) > 0 + # Don't need to iterate through every result. + for copy_number_result in result.copy_number_results[0:2]: + assert copy_number_result.tag_id == result.id + + +def test_Tag_with_data_sets(app, related): + query = return_tag_query('data_sets') + result = query.filter_by(name=related).one_or_none() + + assert result + assert isinstance(result.data_sets, list) + assert len(result.data_sets) > 0 + # Don't need to iterate through every result. + for data_set in result.data_sets[0:2]: + assert type(data_set.name) is str + + +def test_Tag_with_dataset_tag_assoc(app, related): + query = return_tag_query('dataset_tag_assoc') + result = query.filter_by(name=related).one_or_none() + + assert result + assert isinstance(result.dataset_tag_assoc, list) + assert len(result.dataset_tag_assoc) > 0 + # Don't need to iterate through every result. + for dataset_tag_rel in result.dataset_tag_assoc[0:2]: + assert dataset_tag_rel.tag_id == result.id + + +def test_Tag_with_driver_results(app, tag_name): + query = return_tag_query('driver_results') + result = query.filter_by(name=tag_name).one_or_none() + + assert result + assert isinstance(result.driver_results, list) + assert len(result.driver_results) > 0 + # Don't need to iterate through every result. + for driver_result in result.driver_results[0:2]: + assert driver_result.tag_id == result.id + +def test_Tag_with_publications(app, tag_with_publication): + query = return_tag_query('publications') + result = query.filter_by(name=tag_with_publication).one_or_none() + + assert result + assert len(result.publications) > 0 + # Don't need to iterate through every result. + for publication in result.publications[0:2]: + assert type(publication.title) is str + + +def test_Tag_with_related_tags(app, tag_name): + query = return_tag_query('related_tags') + result = query.filter_by(name=tag_name).one_or_none() + + assert result + assert isinstance(result.related_tags, list) + assert len(result.related_tags) > 0 + # Don't need to iterate through every result. + for related_tag in result.related_tags[0:2]: + assert type(related_tag.name) is str + assert result.name == tag_name + + +def test_Tag_with_samples(app, tag_name): + query = return_tag_query('samples') + result = query.filter_by(name=tag_name).one_or_none() + + assert result + assert isinstance(result.samples, list) + assert len(result.samples) > 0 + # Don't need to iterate through every result. + for sample in result.samples[0:2]: + assert type(sample.name) is str + assert result.name == tag_name + assert type(result.description) is str + assert type(result.color) is str or NoneType + assert type(result.long_display) is str or NoneType + assert type(result.short_display) is str or NoneType + assert repr(result) == '' % tag_name + + +def test_Tag_with_tags(app, related): + query = return_tag_query('tags') + result = query.filter_by(name=related).one_or_none() + + assert result + assert isinstance(result.tags, list) + assert len(result.tags) > 0 + # Don't need to iterate through every result. + for tag in result.tags[0:2]: + assert type(tag.name) is str + + +def test_Tag_with_tag_publication_assoc(app, tag_with_publication): + query = return_tag_query('tag_publication_assoc') + result = query.filter_by(name=tag_with_publication).one_or_none() + + assert result + assert isinstance(result.tag_publication_assoc, list) + assert len(result.tag_publication_assoc) > 0 + # Don't need to iterate through every result. + for tag_publication_rel in result.tag_publication_assoc[0:2]: + assert tag_publication_rel.tag_id == result.id diff --git a/apps/iatlas/api-gitlab/tests/db_models/test_TagToPublication.py b/apps/iatlas/api-gitlab/tests/db_models/test_TagToPublication.py new file mode 100644 index 0000000000..971b66a572 --- /dev/null +++ b/apps/iatlas/api-gitlab/tests/db_models/test_TagToPublication.py @@ -0,0 +1,58 @@ +import pytest +from api.database import return_tag_to_publication_query + + +@pytest.fixture(scope='module') +def publication_title_with_tag(): + return 'Comprehensive Pan-Genomic Characterization of Adrenocortical Carcinoma.' + + +@pytest.fixture(scope='module') +def publication_id_with_tag(test_db, publication_title_with_tag): + from api.db_models import Publication + (id, ) = test_db.session.query(Publication.id).filter_by( + title=publication_title_with_tag).one_or_none() + return id + + +def test_TagToPublication_with_relations(app, publication_title_with_tag, publication_id_with_tag): + string_representation_list = [] + separator = ', ' + + query = return_tag_to_publication_query('publications', 'tags') + results = query.filter_by(publication_id=publication_id_with_tag).limit(3).all() + + assert isinstance(results, list) + assert len(results) > 0 + for result in results: + string_representation = '' % result.tag_id + string_representation_list.append(string_representation) + assert isinstance(result.publications, list) + assert len(result.publications) > 0 + # Don't need to iterate through every result. + for publication in result.publications[0:5]: + assert publication.id == publication_id_with_tag + assert publication.title == publication_title_with_tag + assert isinstance(result.tags, list) + assert len(result.tags) > 0 + # Don't need to iterate through every result. + for tag in result.tags[0:5]: + assert type(tag.name) is str + assert result.publication_id == publication_id_with_tag + assert type(result.tag_id) is str + assert repr(result) == string_representation + assert repr(results) == '[' + separator.join( + string_representation_list) + ']' + + +def test_TagToPublication_no_relations(app, publication_id_with_tag): + query = return_tag_to_publication_query() + results = query.filter_by(publication_id=publication_id_with_tag).limit(3).all() + + assert isinstance(results, list) + assert len(results) > 0 + for result in results[0:5]: + assert result.publications == [] + assert result.tags == [] + assert result.publication_id == publication_id_with_tag + assert type(result.tag_id) is str diff --git a/apps/iatlas/api-gitlab/tests/db_models/test_TagToTag.py b/apps/iatlas/api-gitlab/tests/db_models/test_TagToTag.py new file mode 100644 index 0000000000..a595f21809 --- /dev/null +++ b/apps/iatlas/api-gitlab/tests/db_models/test_TagToTag.py @@ -0,0 +1,59 @@ +import pytest +from api.database import return_tag_to_tag_query +from api.db_models import TagToTag + + +@pytest.fixture(scope='module') +def tt_tag(test_db): + return 'AML_3' + + +@pytest.fixture(scope='module') +def tt_tag_id(test_db, tt_tag): + from api.db_models import Tag + (id, ) = test_db.session.query(Tag.id).filter_by( + name=tt_tag).one_or_none() + return id + + +def test_TagToTag_with_relations(app, tt_tag, tt_tag_id): + string_representation_list = [] + separator = ', ' + + query = return_tag_to_tag_query('related_tags', 'tags') + results = query.filter_by(tag_id=tt_tag_id).limit(3).all() + + assert isinstance(results, list) + assert len(results) > 0 + for result in results: + string_representation = '' % tt_tag_id + string_representation_list.append(string_representation) + assert isinstance(result.related_tags, list) + assert len(result.related_tags) > 0 + # Don't need to iterate through every result. + for related_tag in result.related_tags[0:2]: + assert type(related_tag.name) is str + assert isinstance(result.tags, list) + assert len(result.tags) > 0 + # Don't need to iterate through every result. + for tag in result.tags[0:2]: + assert tag.id == tt_tag_id + assert tag.name == tt_tag + assert result.tag_id == tt_tag_id + assert type(result.related_tag_id) is str + assert repr(result) == string_representation + assert repr(results) == '[' + separator.join( + string_representation_list) + ']' + + +def test_TagToTag_no_relations(app, tt_tag_id): + query = return_tag_to_tag_query() + results = query.filter_by(tag_id=tt_tag_id).limit(3).all() + + assert isinstance(results, list) + assert len(results) > 0 + for result in results: + assert result.related_tags == [] + assert result.tags == [] + assert result.tag_id == tt_tag_id + assert type(result.related_tag_id) is str diff --git a/apps/iatlas/api-gitlab/tests/queries/test_cellStats_query.py b/apps/iatlas/api-gitlab/tests/queries/test_cellStats_query.py new file mode 100644 index 0000000000..e50731c8ec --- /dev/null +++ b/apps/iatlas/api-gitlab/tests/queries/test_cellStats_query.py @@ -0,0 +1,181 @@ +import json +import pytest +from tests import NoneType +from api.resolvers.resolver_helpers.paging_utils import from_cursor_hash, to_cursor_hash, Paging +from api.database import return_cell_stat_query + + +@pytest.fixture(scope='module') +def common_query_builder(): + def f(query_fields): + return """query CellStats( + $paging: PagingInput + $distinct:Boolean + $entrez: [Int!] + ) { + cellStats( + paging: $paging + distinct: $distinct + entrez: $entrez + )""" + query_fields + "}" + return f + + +@pytest.fixture(scope='module') +def common_query(common_query_builder): + return common_query_builder( + """ + { + items { + dataSet { name } + gene { entrez } + type + count + avgExpr + percExpr + } + paging { + type + pages + total + startCursor + endCursor + hasPreviousPage + hasNextPage + page + limit + } + error + } + """ + ) + +def test_cell_stats_cursor_pagination_first(client, common_query_builder): + query = common_query_builder("""{ + items { + id + } + paging { + type + pages + total + startCursor + endCursor + hasPreviousPage + hasNextPage + page + limit + } + }""") + num = 10 + response = client.post( + '/api', json={'query': query, 'variables': { + 'paging': {'first': num} + }}) + json_data = json.loads(response.data) + page = json_data['data']['cellStats'] + items = page['items'] + paging = page['paging'] + start = from_cursor_hash(paging['startCursor']) + end = from_cursor_hash(paging['endCursor']) + + assert len(items) == num + assert paging['hasNextPage'] == True + assert paging['hasPreviousPage'] == False + assert start == items[0]['id'] + assert end == items[num - 1]['id'] + + +def test_cell_stats_cursor_pagination_last(client, common_query_builder): + query = common_query_builder("""{ + items { + id + } + paging { + type + pages + total + startCursor + endCursor + hasPreviousPage + hasNextPage + page + limit + } + }""") + num = 10 + response = client.post( + '/api', json={'query': query, 'variables': { + 'paging': { + 'last': num, + 'before': to_cursor_hash(1000) + } + }}) + json_data = json.loads(response.data) + page = json_data['data']['cellStats'] + items = page['items'] + paging = page['paging'] + start = from_cursor_hash(paging['startCursor']) + end = from_cursor_hash(paging['endCursor']) + + assert len(items) == num + assert paging['hasNextPage'] == False + assert paging['hasPreviousPage'] == True + assert start == items[0]['id'] + assert end == items[num - 1]['id'] + + +def test_cell_stats_cursor_distinct_pagination(client, common_query): + page_num = 2 + num = 10 + response = client.post( + '/api', json={'query': common_query, 'variables': { + 'paging': { + 'page': page_num, + 'first': num, + }, + 'distinct': True + }}) + json_data = json.loads(response.data) + page = json_data['data']['cellStats'] + items = page['items'] + + assert len(items) == num + assert page_num == page['paging']['page'] + + +def test_cell_stats_with_entrez(client, common_query): + response = client.post( + '/api', + json={'query': common_query, 'variables': {'entrez': [3001]}} + ) + + json_data = json.loads(response.data) + page = json_data['data']['cellStats'] + results = page['items'] + + assert isinstance(results, list) + assert len(results) > 5 + for result in results[0:5]: + assert isinstance(result['dataSet']['name'], str) + assert result['gene']['entrez'] == 3001 + assert isinstance(result['type'], str) + assert isinstance(result['count'], int) + assert result['avgExpr'] is None or isinstance(result['avgExpr'], float) + assert result['percExpr'] is None or isinstance(result['percExpr'], float) + + +def test_cell_stats_query_with_no_arguments(client, common_query): + response = client.post('/api', json={'query': common_query}) + json_data = json.loads(response.data) + page = json_data['data']['cellStats'] + results = page['items'] + assert isinstance(results, list) + assert len(results) > 10 + for result in results[0:10]: + assert isinstance(result['dataSet']['name'], str) + assert isinstance(result['gene']['entrez'], int) + assert isinstance(result['type'], str) + assert isinstance(result['count'], int) + assert result['avgExpr'] is None or isinstance(result['avgExpr'], float) + assert result['percExpr'] is None or isinstance(result['percExpr'], float) \ No newline at end of file diff --git a/apps/iatlas/api-gitlab/tests/queries/test_cells_query.py b/apps/iatlas/api-gitlab/tests/queries/test_cells_query.py new file mode 100644 index 0000000000..cfc3229c6b --- /dev/null +++ b/apps/iatlas/api-gitlab/tests/queries/test_cells_query.py @@ -0,0 +1,216 @@ +import json +import pytest +from api.resolvers.resolver_helpers.paging_utils import from_cursor_hash, to_cursor_hash, Paging + +@pytest.fixture(scope='module') +def common_query_builder(): + def f(query_fields): + return """query Cells( + $paging: PagingInput + $distinct:Boolean + $cohort: [String!] + $cell: [String!] + ) { + cells( + paging: $paging + distinct: $distinct + cohort: $cohort + cell: $cell + )""" + query_fields + "}" + return f + + +@pytest.fixture(scope='module') +def common_query(common_query_builder): + return common_query_builder( + """ + { + items { + name + type + } + paging { + type + pages + total + startCursor + endCursor + hasPreviousPage + hasNextPage + page + limit + } + error + } + """ + ) + +@pytest.fixture(scope='module') +def feature_query(common_query_builder): + return common_query_builder( + """ + { + items { + name + type + features { + name + value + } + } + paging { + type + pages + total + startCursor + endCursor + hasPreviousPage + hasNextPage + page + limit + } + error + } + """ + ) + + +def test_cells_cursor_pagination_first(client, common_query_builder): + query = common_query_builder("""{ + items { + id + } + paging { + type + pages + total + startCursor + endCursor + hasPreviousPage + hasNextPage + page + limit + } + }""") + num = 10 + response = client.post( + '/api', json={'query': query, 'variables': { + 'paging': {'first': num} + }}) + json_data = json.loads(response.data) + page = json_data['data']['cells'] + + items = page['items'] + paging = page['paging'] + start = from_cursor_hash(paging['startCursor']) + end = from_cursor_hash(paging['endCursor']) + + assert len(items) == num + assert paging['hasNextPage'] == True + assert paging['hasPreviousPage'] == False + assert start == items[0]['id'] + assert end == items[num - 1]['id'] + + +def test_cells_cursor_pagination_last(client, common_query_builder): + query = common_query_builder("""{ + items { + id + } + paging { + type + pages + total + startCursor + endCursor + hasPreviousPage + hasNextPage + page + limit + } + }""") + num = 10 + response = client.post( + '/api', json={'query': query, 'variables': { + 'paging': { + 'last': num, + 'before': to_cursor_hash(1000) + } + }}) + json_data = json.loads(response.data) + page = json_data['data']['cells'] + items = page['items'] + paging = page['paging'] + start = from_cursor_hash(paging['startCursor']) + end = from_cursor_hash(paging['endCursor']) + + assert len(items) == num + assert paging['hasNextPage'] == False + assert paging['hasPreviousPage'] == True + assert start == items[0]['id'] + assert end == items[num - 1]['id'] + + +def test_cells_cursor_distinct_pagination(client, common_query): + page_num = 2 + num = 10 + response = client.post( + '/api', json={'query': common_query, 'variables': { + 'paging': { + 'page': page_num, + 'first': num, + }, + 'distinct': True + }}) + json_data = json.loads(response.data) + page = json_data['data']['cells'] + items = page['items'] + + assert len(items) == num + assert page_num == page['paging']['page'] + + +def test_cell_query_with_no_arguments(client, common_query): + response = client.post('/api', json={'query': common_query}) + json_data = json.loads(response.data) + page = json_data['data']['cells'] + results = page['items'] + assert isinstance(results, list) + assert len(results) > 10 + for result in results[0:10]: + assert isinstance(result['name'], str) + assert isinstance(result['type'], str) + + +def test_cell_query_with_cell(client, common_query): + response = client.post( + '/api', + json={'query': common_query, 'variables': {'cell': ['RU1311A_T_1_165945547864806']}} + ) + json_data = json.loads(response.data) + page = json_data['data']['cells'] + results = page['items'] + assert isinstance(results, list) + assert len(results) == 1 + result = results[0] + assert result['name'] == 'RU1311A_T_1_165945547864806' + assert isinstance(result['type'], str) + + +def test_feature_query_with_cohort(client, feature_query): + response = client.post( + '/api', + json={'query': feature_query, 'variables': {'cohort': ['MSK_Biopsy_Site']}} + ) + json_data = json.loads(response.data) + page = json_data['data']['cells'] + results = page['items'] + assert isinstance(results, list) + assert len(results) > 10 + for result in results[0:10]: + assert isinstance(result['name'], str) + assert isinstance(result['type'], str) + assert isinstance(result['features'], list) + for feature in result['features']: + assert isinstance(feature['name'], str) + assert isinstance(feature['value'], float) diff --git a/apps/iatlas/api-gitlab/tests/queries/test_cohorts_query.py b/apps/iatlas/api-gitlab/tests/queries/test_cohorts_query.py new file mode 100644 index 0000000000..1fe71d0289 --- /dev/null +++ b/apps/iatlas/api-gitlab/tests/queries/test_cohorts_query.py @@ -0,0 +1,431 @@ +import json +import pytest +from tests import NoneType +from api.resolvers.resolver_helpers.paging_utils import from_cursor_hash, to_cursor_hash, Paging + + +@pytest.fixture(scope='module') +def common_query_builder(): + def f(query_fields): + return """ + query Cohorts( + $paging: PagingInput + $distinct:Boolean + $cohort: [String!] + $dataSet: [String!] + $tag: [String!] + ) { + cohorts( + paging: $paging + distinct: $distinct + cohort: $cohort + dataSet: $dataSet + tag: $tag + ) + """ + query_fields + "}" + return f + + +@pytest.fixture(scope='module') +def common_query(common_query_builder): + return common_query_builder( + """ + { + items { + name + tag { + name + shortDisplay + longDisplay + } + dataSet { name } + } + paging { + type + pages + total + startCursor + endCursor + hasPreviousPage + hasNextPage + page + limit + } + error + } + """ + ) + + +@pytest.fixture(scope='module') +def samples_query(common_query_builder): + return common_query_builder( + """ + { + items { + name + dataSet { name } + tag { + name + shortDisplay + longDisplay + } + samples{ + name + tag { + name + shortDisplay + longDisplay + } + } + } + } + """) + + +def test_cohorts_cursor_pagination_first(client, common_query_builder): + query = common_query_builder("""{ + items { + id + } + paging { + type + pages + total + startCursor + endCursor + hasPreviousPage + hasNextPage + page + limit + } + }""") + num = 5 + response = client.post( + '/api', json={'query': query, 'variables': { + 'paging': {'first': num} + }}) + json_data = json.loads(response.data) + page = json_data['data']['cohorts'] + items = page['items'] + paging = page['paging'] + start = from_cursor_hash(paging['startCursor']) + end = from_cursor_hash(paging['endCursor']) + + assert len(items) == num + assert paging['hasNextPage'] == True + assert paging['hasPreviousPage'] == False + assert start == items[0]['id'] + assert end == items[num - 1]['id'] + + +def test_cohorts_cursor_pagination_last(client, common_query_builder): + query = common_query_builder("""{ + items { + id + } + paging { + type + pages + total + startCursor + endCursor + hasPreviousPage + hasNextPage + page + limit + } + }""") + num = 5 + response = client.post( + '/api', json={'query': query, 'variables': { + 'paging': { + 'last': num + } + }}) + json_data = json.loads(response.data) + page = json_data['data']['cohorts'] + items = page['items'] + paging = page['paging'] + start = from_cursor_hash(paging['startCursor']) + end = from_cursor_hash(paging['endCursor']) + + assert len(items) == num + assert paging['hasNextPage'] == False + assert paging['hasPreviousPage'] == True + assert start == items[0]['id'] + assert end == items[num - 1]['id'] + + +def test_cohorts_cursor_distinct_pagination(client, common_query): + page_num = 2 + num = 2 + response = client.post( + '/api', json={'query': common_query, 'variables': { + 'paging': { + 'page': page_num, + 'first': num, + }, + 'distinct': True, + 'dataSet': ['TCGA'] + }}) + json_data = json.loads(response.data) + page = json_data['data']['cohorts'] + items = page['items'] + + assert len(items) == num + assert page_num == page['paging']['page'] + + +def test_tag_cohort_query_by_name(client, common_query, tcga_tag_cohort_name, data_set, related3): + response = client.post('/api', json={'query': common_query, 'variables': { + 'cohort': [tcga_tag_cohort_name] + }}) + json_data = json.loads(response.data) + page = json_data['data']['cohorts'] + results = page['items'] + assert isinstance(results, list) + assert len(results) == 1 + result = results[0] + assert result['dataSet']['name'] == data_set + assert result['tag']['name'] == related3 + assert result['name'] == tcga_tag_cohort_name + assert type(result['tag']['longDisplay']) is str + assert type(result['tag']['shortDisplay']) is str + + +def test_tag_cohort_query_by_dataset_and_tag(client, common_query, tcga_tag_cohort_name, data_set, related3): + response = client.post('/api', json={'query': common_query, 'variables': { + 'dataSet': [data_set], + 'tag': [related3] + }}) + json_data = json.loads(response.data) + page = json_data['data']['cohorts'] + results = page['items'] + assert isinstance(results, list) + assert len(results) == 1 + result = results[0] + assert result['dataSet']['name'] == data_set + assert result['tag']['name'] == related3 + assert result['name'] == tcga_tag_cohort_name + + +def test_dataset_cohort_query_by_name(client, common_query, pcawg_cohort_name): + response = client.post('/api', json={'query': common_query, 'variables': { + 'cohort': [pcawg_cohort_name] + }}) + json_data = json.loads(response.data) + page = json_data['data']['cohorts'] + results = page['items'] + assert isinstance(results, list) + assert len(results) == 1 + result = results[0] + assert result['dataSet']['name'] == pcawg_cohort_name + assert type(result['tag']) is NoneType + assert result['name'] == pcawg_cohort_name + + +def test_dataset_cohort_query_by_dataset(client, common_query, pcawg_cohort_name): + response = client.post('/api', json={'query': common_query, 'variables': { + 'cohort': [pcawg_cohort_name], + }}) + json_data = json.loads(response.data) + page = json_data['data']['cohorts'] + results = page['items'] + assert isinstance(results, list) + assert len(results) == 1 + result = results[0] + assert result['dataSet']['name'] == pcawg_cohort_name + assert type(result['tag']) is NoneType + assert result['name'] == pcawg_cohort_name + + +def test_dataset_cohort_samples_query(client, samples_query, pcawg_cohort_name): + response = client.post('/api', json={'query': samples_query, 'variables': { + 'cohort': [pcawg_cohort_name] + }}) + json_data = json.loads(response.data) + page = json_data['data']['cohorts'] + results = page['items'] + result = results[0] + assert result['name'] == pcawg_cohort_name + assert result['dataSet']['name'] == pcawg_cohort_name + assert type(result['tag']) is NoneType + assert isinstance(results, list) + assert len(results) == 1 + samples = results[0]['samples'] + assert len(samples) > 1 + for sample in samples[0:2]: + assert type(sample['name']) is str + assert type(sample['tag']) is NoneType + + +def test_tag_cohort_samples_query(client, samples_query, tcga_tag_cohort_name, data_set, related3): + response = client.post('/api', json={'query': samples_query, 'variables': { + 'cohort': [tcga_tag_cohort_name] + }}) + json_data = json.loads(response.data) + page = json_data['data']['cohorts'] + results = page['items'] + result = results[0] + assert result['name'] == tcga_tag_cohort_name + assert result['dataSet']['name'] == data_set + assert result['tag']['name'] == related3 + assert type(result['tag']['shortDisplay']) is str + assert type(result['tag']['longDisplay']) is str + assert isinstance(results, list) + assert len(results) == 1 + samples = results[0]['samples'] + assert len(samples) > 1 + for sample in samples[0:2]: + assert type(sample['name']) is str + assert type(sample['tag']['name']) is str + assert type(sample['tag']['shortDisplay']) is str + assert type(sample['tag']['longDisplay']) is str + + +def test_tcga_cohort_samples_query(client, samples_query, data_set): + response = client.post('/api', json={'query': samples_query, 'variables': { + 'cohort': [data_set] + }}) + json_data = json.loads(response.data) + page = json_data['data']['cohorts'] + results = page['items'] + result = results[0] + assert result['name'] == data_set + assert result['dataSet']['name'] == data_set + assert type(result['tag']) is NoneType + assert isinstance(results, list) + assert len(results) == 1 + samples = results[0]['samples'] + assert len(samples) > 1 + for sample in samples[0:2]: + assert type(sample['name']) is str + assert type(sample['tag']) is NoneType + + +def test_pcawg_cohort_samples_query(client, samples_query, pcawg_data_set): + response = client.post('/api', json={'query': samples_query, 'variables': { + 'cohort': [pcawg_data_set] + }}) + json_data = json.loads(response.data) + page = json_data['data']['cohorts'] + results = page['items'] + result = results[0] + assert result['name'] == pcawg_data_set + assert result['dataSet']['name'] == pcawg_data_set + assert type(result['tag']) is NoneType + assert isinstance(results, list) + assert len(results) == 1 + samples = results[0]['samples'] + assert len(samples) > 1 + for sample in samples[0:2]: + assert type(sample['name']) is str + assert type(sample['tag']) is NoneType + + +def test_tag_cohort_features_query(client, common_query_builder, tcga_tag_cohort_name, data_set, related3): + query = common_query_builder( + """ + { + items { + name + tag { name } + dataSet { name } + features { + name + display + } + } + } + """) + response = client.post('/api', json={'query': query, 'variables': { + 'cohort': [tcga_tag_cohort_name] + }}) + json_data = json.loads(response.data) + page = json_data['data']['cohorts'] + results = page['items'] + result = results[0] + assert result['dataSet']['name'] == data_set + assert result['tag']['name'] == related3 + assert result['name'] == tcga_tag_cohort_name + assert isinstance(results, list) + assert len(results) == 1 + features = results[0]['features'] + assert len(features) > 1 + for feature in features[0:2]: + assert type(feature['name']) is str + assert type(feature['display']) is str + + +def test_tag_cohort_genes_query(client, common_query_builder, tcga_tag_cohort_name, data_set, related3): + query = common_query_builder( + """ + { + items { + name + tag { name } + dataSet { name } + genes { + hgnc + entrez + } + } + } + """) + response = client.post('/api', json={'query': query, 'variables': { + 'cohort': [tcga_tag_cohort_name] + }}) + json_data = json.loads(response.data) + page = json_data['data']['cohorts'] + results = page['items'] + result = results[0] + assert result['dataSet']['name'] == data_set + assert result['tag']['name'] == related3 + assert result['name'] == tcga_tag_cohort_name + assert isinstance(results, list) + assert len(results) == 1 + genes = results[0]['genes'] + assert len(genes) > 1 + for gene in genes[0:2]: + assert type(gene['hgnc']) is str + assert type(gene['entrez']) is int + + +def test_tag_cohort_mutations_query(client, common_query_builder, tcga_tag_cohort_name, data_set, related3): + query = common_query_builder( + """ + { + items { + name + tag { name } + dataSet { name } + mutations { + mutationCode + gene { + entrez + hgnc + } + } + } + } + """ + ) + response = client.post('/api', json={'query': query, 'variables': { + 'cohort': [tcga_tag_cohort_name] + }}) + json_data = json.loads(response.data) + page = json_data['data']['cohorts'] + results = page['items'] + result = results[0] + assert result['dataSet']['name'] == data_set + assert result['tag']['name'] == related3 + assert result['name'] == tcga_tag_cohort_name + assert isinstance(results, list) + assert len(results) == 1 + mutations = results[0]['mutations'] + assert len(mutations) > 1 + for mutation in mutations[0:2]: + assert type(mutation['mutationCode']) is str + assert type(mutation['gene']['hgnc']) is str + assert type(mutation['gene']['entrez']) is int diff --git a/apps/iatlas/api-gitlab/tests/queries/test_colocalizations_query.py b/apps/iatlas/api-gitlab/tests/queries/test_colocalizations_query.py new file mode 100644 index 0000000000..5c51e10503 --- /dev/null +++ b/apps/iatlas/api-gitlab/tests/queries/test_colocalizations_query.py @@ -0,0 +1,258 @@ +import json +import pytest +from tests import NoneType +from api.resolvers.resolver_helpers.paging_utils import from_cursor_hash, to_cursor_hash, Paging +from api.database import return_colocalization_query + + +@pytest.fixture(scope='module') +def coloc_data_set(test_db): + return "GTEX" + + +@pytest.fixture(scope='module') +def coloc_feature(test_db): + return 'Bindea_aDC' + + +@pytest.fixture(scope='module') +def coloc_gene_entrez(test_db): + return 4677 + + +@pytest.fixture(scope='module') +def coloc_snp_name(test_db): + return "18:55726795:A:T" + + +@pytest.fixture(scope='module') +def coloc_qtl_type(test_db): + return "eQTL" + + +@pytest.fixture(scope='module') +def coloc_ecaviar_pp(test_db): + return "C2" + + +@pytest.fixture(scope='module') +def coloc_plot_type(test_db): + return "Expanded Region" + + +@pytest.fixture(scope='module') +def coloc_tissue(test_db): + return "Artery Aorta" + + +@pytest.fixture(scope='module') +def common_query_builder(): + def f(query_fields): + return """query Colocalizations( + $paging: PagingInput + $distinct:Boolean + $dataSet: [String!] + $colocDataSet: [String!] + $feature: [String!] + $entrez: [Int!] + $snp: [String!] + $qtlType: QTLTypeEnum + $eCaviarPP: ECaviarPPEnum + $plotType: ColocPlotTypeEnum + ) { + colocalizations( + paging: $paging + distinct: $distinct + dataSet: $dataSet + colocDataSet: $colocDataSet + feature: $feature + entrez: $entrez + snp: $snp + qtlType: $qtlType + eCaviarPP: $eCaviarPP + plotType: $plotType + )""" + query_fields + "}" + return f + + +@pytest.fixture(scope='module') +def common_query(common_query_builder): + return common_query_builder( + """ + { + items { + dataSet { name } + colocDataSet { name } + feature { name } + gene { entrez } + snp { name } + qtlType + eCaviarPP + plotType + tissue + spliceLoc + plotLink + } + paging { + type + pages + total + startCursor + endCursor + hasPreviousPage + hasNextPage + page + limit + } + error + } + """ + ) + +# Test that forward cursor pagination gives us the expected paginInfo + + +def test_colocalizations_cursor_pagination_first(client, common_query_builder): + query = common_query_builder("""{ + items { + id + } + paging { + type + pages + total + startCursor + endCursor + hasPreviousPage + hasNextPage + page + limit + } + }""") + num = 10 + response = client.post( + '/api', json={'query': query, 'variables': { + 'paging': {'first': num} + }}) + json_data = json.loads(response.data) + page = json_data['data']['colocalizations'] + items = page['items'] + paging = page['paging'] + start = from_cursor_hash(paging['startCursor']) + end = from_cursor_hash(paging['endCursor']) + + assert len(items) == num + assert paging['hasNextPage'] == True + assert paging['hasPreviousPage'] == False + assert start == items[0]['id'] + assert end == items[num - 1]['id'] + + +def test_colocalizations_cursor_pagination_last(client, common_query_builder): + query = common_query_builder("""{ + items { + id + } + paging { + type + pages + total + startCursor + endCursor + hasPreviousPage + hasNextPage + page + limit + } + }""") + num = 10 + response = client.post( + '/api', json={'query': query, 'variables': { + 'paging': { + 'last': num, + 'before': to_cursor_hash(1000) + } + }}) + json_data = json.loads(response.data) + page = json_data['data']['colocalizations'] + items = page['items'] + paging = page['paging'] + start = from_cursor_hash(paging['startCursor']) + end = from_cursor_hash(paging['endCursor']) + + assert len(items) == num + assert paging['hasNextPage'] == False + assert paging['hasPreviousPage'] == True + assert start == items[0]['id'] + assert end == items[num - 1]['id'] + + +def test_colocalizations_cursor_distinct_pagination(client, common_query): + page_num = 2 + num = 10 + response = client.post( + '/api', json={'query': common_query, 'variables': { + 'paging': { + 'page': page_num, + 'first': num, + }, + 'distinct': True, + 'dataSet': ['TCGA'] + }}) + json_data = json.loads(response.data) + page = json_data['data']['colocalizations'] + items = page['items'] + + assert len(items) == num + assert page_num == page['paging']['page'] + + +def test_colocalizations_unique_query(client, common_query, data_set, coloc_data_set, coloc_feature, coloc_gene_entrez, coloc_snp_name, coloc_qtl_type, coloc_ecaviar_pp, coloc_plot_type, coloc_tissue): + response = client.post('/api', json={'query': common_query, 'variables': { + 'dataSet': [data_set], + 'colocDataSet': [coloc_data_set], + 'feature': [coloc_feature], + 'entrez': [coloc_gene_entrez], + 'snp': [coloc_snp_name], + 'qtlType': coloc_qtl_type + }}) + + json_data = json.loads(response.data) + page = json_data['data']['colocalizations'] + results = page['items'] + + assert isinstance(results, list) + assert len(results) == 1 + for result in results: + assert result['dataSet']['name'] == data_set + assert result['colocDataSet']['name'] == coloc_data_set + assert result['feature']['name'] == coloc_feature + assert result['gene']['entrez'] == coloc_gene_entrez + assert result['snp']['name'] == coloc_snp_name + assert result['qtlType'] == coloc_qtl_type + assert type(result['eCaviarPP']) is NoneType + assert result['plotType'] == coloc_plot_type + assert result['tissue'] == coloc_tissue + assert type(result['spliceLoc']) is NoneType + assert type(result['plotLink']) is str + + +def test_colocalizations_query_with_no_arguments(client, common_query): + response = client.post('/api', json={'query': common_query}) + json_data = json.loads(response.data) + page = json_data['data']['colocalizations'] + results = page['items'] + assert isinstance(results, list) + assert len(results) > 10 + for result in results[1:10]: + assert type(result['dataSet']['name']) is str + assert type(result['colocDataSet']['name']) is str + assert type(result['feature']['name']) is str + assert type(result['gene']['entrez']) is int + assert type(result['snp']['name']) is str + assert type(result['qtlType']) is str + assert type(result['eCaviarPP']) is str or NoneType + assert type(result['plotType']) is str or NoneType + assert type(result['tissue']) is str or NoneType + assert type(result['spliceLoc']) is str or NoneType + assert type(result['plotLink']) is str or NoneType diff --git a/apps/iatlas/api-gitlab/tests/queries/test_copyNumberResults_query.py b/apps/iatlas/api-gitlab/tests/queries/test_copyNumberResults_query.py new file mode 100644 index 0000000000..aef76d15f7 --- /dev/null +++ b/apps/iatlas/api-gitlab/tests/queries/test_copyNumberResults_query.py @@ -0,0 +1,660 @@ +import json +import pytest +from tests import NoneType +from api.enums import direction_enum +from api.resolvers.resolver_helpers.paging_utils import from_cursor_hash, to_cursor_hash, Paging + +""" +query CopyNumberResults( + $paging: PagingInput + $distinct:Boolean + $dataSet: [String!] + $related: [String!] + $feature: [String!] + $entrez: [Int!] + $tag: [String!] + $direction: DirectionEnum + $minPValue: Float + $maxPValue: Float + $minLog10PValue: Float + $maxLog10PValue: Float + $minMeanNormal: Float + $minMeanCnv: Float + $minTStat: Float +) { + copyNumberResults( + paging: $paging + distinct: $distinct + dataSet: $dataSet + related: $related + feature: $feature + entrez: $entrez + tag: $tag + direction: $direction + minPValue: $minPValue + maxPValue: $maxPValue + minLog10PValue: $minLog10PValue + maxLog10PValue: $maxLog10PValue + minMeanNormal: $minMeanNormal + minMeanCnv: $minMeanCnv + minTStat: $minTStat + ) { + paging { + type + pages + total + startCursor + endCursor + hasPreviousPage + hasNextPage + page + limit + } + error + items { + id + direction + meanNormal + meanCnv + pValue + log10PValue + tStat + dataSet { + name + } + tag { + name + } + gene { + entrez + hgnc + } + feature { + name + } + } + } +} +""" + + +@pytest.fixture(scope='module') +def test_cnr(): + return { + "dataset": "TCGA", + "tag": "C1", + "entrez_id": 55303, + "feature": "Subclonal_genome_fraction", + "direction": "Amp" + } + + +@pytest.fixture(scope='module') +def min_p_value(): + return 0.0000028 + + +@pytest.fixture(scope='module') +def max_p_value(): + return 0.000021 + + +@pytest.fixture(scope='module') +def min_log10_p_value(): + return 0.000037 + + +@pytest.fixture(scope='module') +def max_log10_p_value(): + return 13.162428 + + +@pytest.fixture(scope='module') +def min_mean_normal(): + return 9.313083 + + +@pytest.fixture(scope='module') +def min_mean_cnv(): + return 14.833332 + + +@pytest.fixture(scope='module') +def min_t_stat(): + return -5.118745 + + +@pytest.fixture(scope='module') +def common_query_builder(): + def f(query_fields): + return """query CopyNumberResults( + $paging: PagingInput + $distinct:Boolean + $dataSet: [String!] + $related: [String!] + $feature: [String!] + $entrez: [Int!] + $tag: [String!] + $direction: DirectionEnum + $minPValue: Float + $maxPValue: Float + $minLog10PValue: Float + $maxLog10PValue: Float + $minMeanNormal: Float + $minMeanCnv: Float + $minTStat: Float + ) { + copyNumberResults( + paging: $paging + distinct: $distinct + dataSet: $dataSet + related: $related + feature: $feature + entrez: $entrez + tag: $tag + direction: $direction + minPValue: $minPValue + maxPValue: $maxPValue + minLog10PValue: $minLog10PValue + maxLog10PValue: $maxLog10PValue + minMeanNormal: $minMeanNormal + minMeanCnv: $minMeanCnv + minTStat: $minTStat + )""" + query_fields + "}" + return f + + +# Test that forward cursor pagination gives us the expected pagingInfo + + +def test_copyNumberResults_cursor_pagination_first(client, common_query_builder): + query = common_query_builder("""{ + paging { + startCursor + endCursor + hasNextPage + hasPreviousPage + } + items { id } + }""") + num = 10 + response = client.post( + '/api', json={'query': query, 'variables': { + 'paging': {'first': num} + }}) + json_data = json.loads(response.data) + page = json_data['data']['copyNumberResults'] + items = page['items'] + paging = page['paging'] + start = from_cursor_hash(paging['startCursor']) + end = from_cursor_hash(paging['endCursor']) + + assert len(items) == num + assert paging['hasNextPage'] == True + assert paging['hasPreviousPage'] == False + assert start == items[0]['id'] + assert end == items[num - 1]['id'] + + +def test_copyNumberResults_cursor_pagination_last(client, common_query_builder): + query = common_query_builder("""{ + paging { + startCursor + endCursor + hasNextPage + hasPreviousPage + } + items { id } + }""") + num = 10 + response = client.post( + '/api', json={'query': query, 'variables': { + 'paging': { + 'last': num, + 'before': to_cursor_hash(100) + } + }}) + json_data = json.loads(response.data) + page = json_data['data']['copyNumberResults'] + items = page['items'] + paging = page['paging'] + start = from_cursor_hash(paging['startCursor']) + end = from_cursor_hash(paging['endCursor']) + + assert len(items) == num + assert paging['hasNextPage'] == False + assert paging['hasPreviousPage'] == True + assert start == items[0]['id'] + assert end == items[num - 1]['id'] + + +def test_copyNumberResults_cursor_distinct_pagination(client, common_query_builder): + query = common_query_builder("""{ + paging { page } + items { pValue } + }""") + page_num = 2 + num = 10 + response = client.post( + '/api', json={'query': query, 'variables': { + 'paging': { + 'page': page_num, + 'first': num, + }, + 'distinct': True, + 'dataSet': ['TCGA'], + 'tag': ['C1'] + }}) + json_data = json.loads(response.data) + page = json_data['data']['copyNumberResults'] + items = page['items'] + + assert len(items) == num + assert page_num == page['paging']['page'] + + +def test_copyNumberResults_missing_pagination(client, common_query_builder): + """Verify that query does not error when paging is not sent by the client + + The purpose of this test is the ensure that valid and sensible default values + are used and the query does not error, when no paging arguments are sent. + Cursor pagination and a limit of 100,000 will be used by default. + """ + query = common_query_builder("""{ items { pValue } }""") + response = client.post( + '/api', json={'query': query, 'variables': { + 'dataSet': ['TCGA'] + }}) + json_data = json.loads(response.data) + page = json_data['data']['copyNumberResults'] + items = page['items'] + + assert len(items) == 10000 + + +def test_copyNumberResults_query_with_passed_data_set(client, common_query_builder, test_cnr): + query = common_query_builder("""{ + paging { + total + startCursor + endCursor + hasPreviousPage + hasNextPage + } + items { + dataSet { name } + } + }""") + response = client.post( + '/api', json={'query': query, 'variables': { + 'paging': {'first': 10}, + 'dataSet': [test_cnr["dataset"]], + 'entrez': [test_cnr["entrez_id"]], + 'cnr_feature': [test_cnr["feature"]] + }}) + json_data = json.loads(response.data) + page = json_data['data']['copyNumberResults'] + paging = page['paging'] + items = page['items'] + + assert type(paging['total']) is int + assert paging['hasNextPage'] == True + assert paging['hasPreviousPage'] == False + assert type(paging['startCursor']) is str + assert type(paging['endCursor']) is str + + assert isinstance(items, list) + assert len(items) > 0 + for item in items[0:2]: + current_data_set = item['dataSet'] + assert current_data_set['name'] == test_cnr["dataset"] + + +def test_copyNumberResults_query_with_passed_related(client, common_query_builder, test_cnr, min_t_stat, related): + query = common_query_builder("""{ + items { tStat } + }""") + response = client.post( + '/api', json={'query': query, 'variables': { + 'dataSet': [test_cnr["dataset"]], + 'entrez': [test_cnr["entrez_id"]], + 'minTStat': min_t_stat, + 'related': ['does_not_exist'], + 'tag': [test_cnr["tag"]] + }}) + json_data = json.loads(response.data) + page = json_data['data']['copyNumberResults'] + results = page['items'] + + assert isinstance(results, list) + assert len(results) == 0 + + response = client.post( + '/api', json={'query': query, 'variables': { + 'dataSet': [test_cnr["dataset"]], + 'entrez': [test_cnr["entrez_id"]], + 'minTStat': min_t_stat, + 'related': [related], + 'tag': [test_cnr["tag"]] + }}) + json_data = json.loads(response.data) + page = json_data['data']['copyNumberResults'] + results = page['items'] + + assert isinstance(results, list) + assert len(results) > 0 + for result in results[0:2]: + assert result['tStat'] >= min_t_stat + + +def test_copyNumberResults_query_with_passed_entrez(client, common_query_builder, test_cnr): + query = common_query_builder("""{ + items { + gene { entrez } + } + }""") + response = client.post( + '/api', json={'query': query, 'variables': { + 'dataSet': [test_cnr["dataset"]], + 'entrez': [test_cnr["entrez_id"]], + 'feature': [test_cnr["feature"]] + }}) + json_data = json.loads(response.data) + page = json_data['data']['copyNumberResults'] + results = page['items'] + + assert isinstance(results, list) + assert len(results) > 0 + for result in results[0:2]: + gene = result['gene'] + assert gene['entrez'] == test_cnr["entrez_id"] + + +def test_copyNumberResults_query_with_passed_features(client, common_query_builder, test_cnr): + query = common_query_builder("""{ + items { + feature { name } + } + }""") + response = client.post( + '/api', json={'query': query, 'variables': { + 'dataSet': [test_cnr["dataset"]], + 'entrez': [test_cnr["entrez_id"]], + 'feature': [test_cnr["feature"]] + }}) + json_data = json.loads(response.data) + page = json_data['data']['copyNumberResults'] + results = page['items'] + + assert isinstance(results, list) + assert len(results) > 0 + for result in results[0:2]: + feature = result['feature'] + assert feature['name'] == test_cnr["feature"] + + +def test_copyNumberResults_query_with_passed_tag(client, common_query_builder, test_cnr): + query = common_query_builder("""{ + items { + tag { name } + } + }""") + response = client.post( + '/api', json={'query': query, 'variables': { + 'dataSet': [test_cnr["dataset"]], + 'feature': [test_cnr["feature"]], + 'tag': [test_cnr["tag"]], + 'paging': {'first': 100000} + }}) + json_data = json.loads(response.data) + page = json_data['data']['copyNumberResults'] + results = page['items'] + + assert isinstance(results, list) + assert len(results) > 0 + for result in results[0:2]: + tag = result['tag'] + assert tag['name'] == test_cnr["tag"] + + +def test_copyNumberResults_query_with_passed_direction(client, common_query_builder, test_cnr): + query = common_query_builder("""{ + items { direction } + }""") + response = client.post( + '/api', json={'query': query, 'variables': { + 'dataSet': [test_cnr["dataset"]], + 'direction': test_cnr["direction"], + 'entrez': [test_cnr["entrez_id"]], + 'tag': [test_cnr["tag"]] + }}) + json_data = json.loads(response.data) + page = json_data['data']['copyNumberResults'] + results = page['items'] + + assert isinstance(results, list) + assert len(results) > 0 + for result in results[0:2]: + assert result['direction'] == test_cnr["direction"] + + +def test_copyNumberResults_query_with_passed_min_p_value(client, common_query_builder, test_cnr, min_p_value): + query = common_query_builder("""{ + items { pValue } + }""") + response = client.post( + '/api', json={'query': query, 'variables': { + 'dataSet': [test_cnr["dataset"]], + 'entrez': [test_cnr["entrez_id"]], + 'minPValue': min_p_value, + 'tag': [test_cnr["tag"]] + }}) + json_data = json.loads(response.data) + page = json_data['data']['copyNumberResults'] + results = page['items'] + + assert isinstance(results, list) + assert len(results) > 0 + for result in results[0:2]: + assert result['pValue'] >= min_p_value + + +def test_copyNumberResults_query_with_passed_min_p_value_and_min_log10_p_value(client, common_query_builder, test_cnr, min_log10_p_value, min_p_value): + query = common_query_builder("""{ + items { pValue } + }""") + response = client.post( + '/api', json={'query': query, 'variables': { + 'dataSet': [test_cnr["dataset"]], + 'entrez': [test_cnr["entrez_id"]], + 'minLog10PValue': min_log10_p_value, + 'minPValue': min_p_value, + 'tag': [test_cnr["tag"]], + 'paging': {'first': 10000} + }}) + json_data = json.loads(response.data) + page = json_data['data']['copyNumberResults'] + results = page['items'] + + assert isinstance(results, list) + assert len(results) > 0 + for result in results[0:2]: + assert result['pValue'] >= min_p_value + + +def test_copyNumberResults_query_with_passed_max_p_value(client, common_query_builder, test_cnr, max_p_value): + query = common_query_builder("""{ + items { pValue } + }""") + response = client.post( + '/api', json={'query': query, 'variables': { + 'dataSet': [test_cnr["dataset"]], + 'entrez': [test_cnr["entrez_id"]], + 'maxPValue': max_p_value, + 'tag': [test_cnr["tag"]] + }}) + json_data = json.loads(response.data) + page = json_data['data']['copyNumberResults'] + results = page['items'] + + assert isinstance(results, list) + assert len(results) > 0 + for result in results[0:2]: + assert result['pValue'] <= max_p_value + + +def test_copyNumberResults_query_with_passed_max_p_value_and_max_log10_p_value(client, common_query_builder, test_cnr, max_log10_p_value, max_p_value): + query = common_query_builder("""{ + items { pValue } + }""") + response = client.post( + '/api', json={'query': query, 'variables': { + 'dataSet': [test_cnr["dataset"]], + 'entrez': [test_cnr["entrez_id"]], + 'maxLog10PValue': max_log10_p_value, + 'maxPValue': max_p_value, + 'tag': [test_cnr["tag"]] + }}) + json_data = json.loads(response.data) + page = json_data['data']['copyNumberResults'] + results = page['items'] + + assert isinstance(results, list) + assert len(results) > 0 + for result in results[0:2]: + assert result['pValue'] <= max_p_value + + +def test_copyNumberResults_query_with_passed_min_log10_p_value(client, common_query_builder, test_cnr, min_log10_p_value): + query = common_query_builder("""{ + items { log10PValue } + }""") + response = client.post( + '/api', json={'query': query, 'variables': { + 'dataSet': [test_cnr["dataset"]], + 'entrez': [test_cnr["entrez_id"]], + 'minLog10PValue': min_log10_p_value, + 'tag': [test_cnr["tag"]] + }}) + json_data = json.loads(response.data) + page = json_data['data']['copyNumberResults'] + results = page['items'] + + assert isinstance(results, list) + assert len(results) > 0 + for result in results[0:2]: + assert result['log10PValue'] >= min_log10_p_value + + +def test_copyNumberResults_query_with_passed_max_log10_p_value(client, common_query_builder, test_cnr, max_log10_p_value): + query = common_query_builder("""{ + items { log10PValue } + }""") + response = client.post( + '/api', json={'query': query, 'variables': { + 'dataSet': [test_cnr["dataset"]], + 'entrez': [test_cnr["entrez_id"]], + 'maxLog10PValue': max_log10_p_value, + 'tag': [test_cnr["tag"]] + }}) + json_data = json.loads(response.data) + page = json_data['data']['copyNumberResults'] + results = page['items'] + + assert isinstance(results, list) + assert len(results) > 0 + for result in results[0:2]: + assert result['log10PValue'] <= max_log10_p_value + + +def test_copyNumberResults_query_with_passed_min_mean_normal(client, common_query_builder, test_cnr, min_mean_normal): + query = common_query_builder("""{ + items { meanNormal } + }""") + response = client.post( + '/api', json={'query': query, 'variables': { + 'dataSet': [test_cnr["dataset"]], + 'entrez': [test_cnr["entrez_id"]], + 'minMeanNormal': min_mean_normal, + 'tag': [test_cnr["tag"]] + }}) + json_data = json.loads(response.data) + page = json_data['data']['copyNumberResults'] + results = page['items'] + + assert isinstance(results, list) + assert len(results) > 0 + for result in results[0:2]: + assert result['meanNormal'] >= min_mean_normal + + +def test_copyNumberResults_query_with_passed_min_mean_cnv(client, common_query_builder, test_cnr, min_mean_cnv): + query = common_query_builder("""{ + items { meanCnv } + }""") + response = client.post( + '/api', json={'query': query, 'variables': { + 'dataSet': [test_cnr["dataset"]], + 'entrez': [test_cnr["entrez_id"]], + 'minMeanCnv': min_mean_cnv, + 'tag': [test_cnr["tag"]] + }}) + json_data = json.loads(response.data) + page = json_data['data']['copyNumberResults'] + results = page['items'] + + assert isinstance(results, list) + assert len(results) > 0 + for result in results[0:2]: + assert result['meanCnv'] >= min_mean_cnv + + +def test_copyNumberResults_query_with_passed_min_t_stat(client, common_query_builder, test_cnr, min_t_stat): + query = common_query_builder("""{ + items { tStat } + }""") + response = client.post( + '/api', json={'query': query, 'variables': { + 'dataSet': [test_cnr["dataset"]], + 'entrez': [test_cnr["entrez_id"]], + 'minTStat': min_t_stat, + 'tag': [test_cnr["tag"]] + }}) + json_data = json.loads(response.data) + page = json_data['data']['copyNumberResults'] + results = page['items'] + + assert isinstance(results, list) + assert len(results) > 0 + for result in results[0:2]: + assert result['tStat'] >= min_t_stat + + +def test_copyNumberResults_query_with_no_arguments(client, common_query_builder): + query = common_query_builder("""{ + items { + direction + meanNormal + meanCnv + pValue + log10PValue + tStat + } + }""") + response = client.post('/api', json={ + 'query': query, + 'variables': {'paging': {'first': 10000}} + }) + json_data = json.loads(response.data) + page = json_data['data']['copyNumberResults'] + results = page['items'] + + assert isinstance(results, list) + assert len(results) > 0 + for result in results[0:2]: + assert result['direction'] in direction_enum.enums + assert type(result['meanNormal']) is float or NoneType + assert type(result['meanCnv']) is float or NoneType + assert type(result['pValue']) is float or NoneType + assert type(result['log10PValue']) is float or NoneType + assert type(result['tStat']) is int or NoneType diff --git a/apps/iatlas/api-gitlab/tests/queries/test_data_sets_query.py b/apps/iatlas/api-gitlab/tests/queries/test_data_sets_query.py new file mode 100644 index 0000000000..d03ef5f378 --- /dev/null +++ b/apps/iatlas/api-gitlab/tests/queries/test_data_sets_query.py @@ -0,0 +1,256 @@ +import json +import pytest +from tests import NoneType +from api.resolvers.resolver_helpers.paging_utils import from_cursor_hash, to_cursor_hash + + +@pytest.fixture(scope='module') +def data_set_type(): + return 'analysis' + + +@pytest.fixture(scope='module') +def common_query_builder(): + def f(query_fields): + return( + """ + query DataSets( + $paging: PagingInput + $distinct: Boolean + $dataSet: [String!] + $sample: [String!] + $dataSetType: [String!] + ) { + dataSets( + paging: $paging + distinct: $distinct + dataSet: $dataSet + sample: $sample + dataSetType: $dataSetType + ) + """ + query_fields + "}" + ) + return f + + +@pytest.fixture(scope='module') +def common_query(common_query_builder): + return common_query_builder( + """{ + items { + id + display + name + type + } + paging { + type + pages + total + startCursor + endCursor + hasPreviousPage + hasNextPage + page + limit + } + error + }""" + ) + + +@pytest.fixture(scope='module') +def samples_query(common_query_builder): + return common_query_builder( + """{ + items { + id + display + name + type + samples { + name + } + } + }""" + ) + + +@pytest.fixture(scope='module') +def tags_query(common_query_builder): + return common_query_builder( + """{ + items { + id + display + name + type + tags { + name + } + } + }""" + ) + + +def test_data_sets_cursor_pagination_first(client, common_query): + num = 1 + response = client.post( + '/api', json={'query': common_query, 'variables': { + 'paging': {'first': num} + }}) + json_data = json.loads(response.data) + page = json_data['data']['dataSets'] + items = page['items'] + paging = page['paging'] + start = from_cursor_hash(paging['startCursor']) + end = from_cursor_hash(paging['endCursor']) + + assert len(items) == num + assert paging['hasNextPage'] == True + assert paging['hasPreviousPage'] == False + assert start == items[0]['id'] + assert end == items[num - 1]['id'] + +def test_data_sets_cursor_pagination_last(client, common_query): + num = 1 + response = client.post( + '/api', json={'query': common_query, 'variables': { + 'paging': { + 'last': num + } + }}) + json_data = json.loads(response.data) + page = json_data['data']['dataSets'] + items = page['items'] + paging = page['paging'] + start = from_cursor_hash(paging['startCursor']) + end = from_cursor_hash(paging['endCursor']) + assert len(items) == num + assert paging['hasNextPage'] == False + assert paging['hasPreviousPage'] == True + assert start == items[0]['id'] + assert end == items[num - 1]['id'] + + +def test_data_sets_cursor_distinct_pagination(client, common_query): + page_num = 1 + num = 1 + response = client.post( + '/api', json={'query': common_query, 'variables': { + 'paging': { + 'page': page_num, + 'first': num, + }, + 'distinct': True + }}) + json_data = json.loads(response.data) + page = json_data['data']['dataSets'] + items = page['items'] + + assert len(items) == num + assert page_num == page['paging']['page'] + + +def test_data_sets_query_no_args(client, common_query): + response = client.post('/api', json={'query': common_query}) + json_data = json.loads(response.data) + page = json_data['data']['dataSets'] + results = page['items'] + + assert isinstance(results, list) + assert len(results) > 0 + for data_set in results: + assert type(data_set['name']) is str + assert type(data_set['display']) is str or NoneType + + +def test_data_sets_query_with_dataSet(client, samples_query, data_set): + response = client.post( + '/api', json={'query': samples_query, 'variables': {'dataSet': [data_set]}}) + json_data = json.loads(response.data) + page = json_data['data']['dataSets'] + results = page['items'] + + assert isinstance(results, list) + assert len(results) == 1 + for current_data_set in results: + samples = current_data_set['samples'] + + assert current_data_set['name'] == data_set + assert type(current_data_set['display']) is str or NoneType + assert type(current_data_set['type']) is str + assert isinstance(samples, list) + assert len(samples) > 0 + for current_sample in samples[0:5]: + assert type(current_sample['name']) is str + + +def test_data_sets_query_with_sample(client, samples_query, sample): + response = client.post( + '/api', json={'query': samples_query, 'variables': {'sample': [sample]}}) + json_data = json.loads(response.data) + page = json_data['data']['dataSets'] + results = page['items'] + assert isinstance(results, list) + assert len(results) == 1 + for data_set in results: + samples = data_set['samples'] + assert type(data_set['name']) is str + assert type(data_set['display']) is str or NoneType + assert isinstance(samples, list) + assert len(samples) == 1 + for current_sample in samples: + assert current_sample['name'] == sample + + +def test_data_sets_query_with_dataSet_and_sample(client, samples_query, data_set, sample): + response = client.post( + '/api', json={'query': samples_query, 'variables': {'dataSet': [data_set], 'sample': [sample]}}) + json_data = json.loads(response.data) + page = json_data['data']['dataSets'] + results = page['items'] + + assert isinstance(results, list) + assert len(results) == 1 + for current_data_set in results: + samples = current_data_set['samples'] + + assert current_data_set['name'] == data_set + assert type(current_data_set['display']) is str or NoneType + assert isinstance(samples, list) + assert len(samples) == 1 + for current_sample in samples: + assert current_sample['name'] == sample + + +def test_data_sets_query_with_dataSetType(client, common_query, data_set_type): + response = client.post( + '/api', json={'query': common_query, 'variables': {'dataSetType': [data_set_type]}}) + json_data = json.loads(response.data) + page = json_data['data']['dataSets'] + results = page['items'] + + assert isinstance(results, list) + assert len(results) > 0 + for data_set in results: + assert type(data_set['name']) is str + assert type(data_set['display']) is str or NoneType + assert data_set['type'] == data_set_type + + +def test_data_sets_query_with_tags(client, tags_query, data_set): + response = client.post( + '/api', json={'query': tags_query, 'variables': {'dataSet': [data_set]}}) + json_data = json.loads(response.data) + page = json_data['data']['dataSets'] + results = page['items'] + assert isinstance(results, list) + assert len(results) == 1 + for d in results: + assert d['name'] == data_set + tags = d['tags'] + assert isinstance(tags, list) + assert len(tags) > 1 + for tag in tags: + assert type(tag['name']) is str diff --git a/apps/iatlas/api-gitlab/tests/queries/test_driverResults_query.py b/apps/iatlas/api-gitlab/tests/queries/test_driverResults_query.py new file mode 100644 index 0000000000..fee5f31fee --- /dev/null +++ b/apps/iatlas/api-gitlab/tests/queries/test_driverResults_query.py @@ -0,0 +1,660 @@ +import json +import pytest +from tests import NoneType +from api.resolvers.resolver_helpers.paging_utils import from_cursor_hash, to_cursor_hash + + +@pytest.fixture(scope='module') +def dr_feature(): + return 'Module11_Prolif_score' + + +@pytest.fixture(scope='module') +def dr_mutation(): + return 'KANSL1:(OM)' + + +@pytest.fixture(scope='module') +def mutation_code(): + return '(OM)' + + +@pytest.fixture(scope='module') +def gene_entrez(): + return 284058 + + +@pytest.fixture(scope='module') +def gene_hgnc(): + return 'KANSL1' + + +@pytest.fixture(scope='module') +def dr_tag_name(): + return 'BLCA' + + +@pytest.fixture(scope='module') +def max_p_value(): + return 0.495103 + + +@pytest.fixture(scope='module') +def max_log10_p_value(): + return 0.197782 + + +@pytest.fixture(scope='module') +def min_fold_change(): + return 1.44142 + + +@pytest.fixture(scope='module') +def min_log10_fold_change(): + return -0.0544383 + + +@pytest.fixture(scope='module') +def min_p_value(): + return 0.634187 + + +@pytest.fixture(scope='module') +def min_log10_p_value(): + return 0.30530497 + + +@pytest.fixture(scope='module') +def min_n_mut(): + return 23 + + +@pytest.fixture(scope='module') +def min_n_wt(): + return 383 + + +@pytest.fixture(scope='module') +def common_query_builder(): + def f(query_fields): + return """query DriverResults( + $paging: PagingInput + $distinct: Boolean + $dataSet: [String!] + $related: [String!] + $entrez: [Int!] + $feature: [String!] + $mutation: [String!] + $mutationCode: [String!] + $tag: [String!] + $minPValue: Float + $maxPValue: Float + $minLog10PValue: Float + $maxLog10PValue: Float + $minFoldChange: Float + $minLog10FoldChange: Float + $minNumWildTypes: Int + $minNumMutants: Int + ) { + driverResults( + paging: $paging + distinct: $distinct + dataSet: $dataSet + related: $related + feature: $feature + entrez: $entrez + mutation: $mutation + mutationCode: $mutationCode + tag: $tag + minPValue: $minPValue + maxPValue: $maxPValue + minLog10PValue: $minLog10PValue + maxLog10PValue: $maxLog10PValue + minFoldChange: $minFoldChange + minLog10FoldChange: $minLog10FoldChange + minNumWildTypes: $minNumWildTypes + minNumMutants: $minNumMutants + )""" + query_fields + "}" + return f + + +@pytest.fixture(scope='module') +def paging_query(common_query_builder): + return common_query_builder( + """{ + items { + id + } + paging { + type + pages + total + startCursor + endCursor + hasPreviousPage + hasNextPage + page + limit + } + error + }""") + + +@pytest.fixture(scope='module') +def simple_query(common_query_builder): + return common_query_builder( + """{ + items { + pValue + log10PValue + foldChange + log10FoldChange + numWildTypes + numMutants + } + }""") + + +@pytest.fixture(scope='module') +def common_query(common_query_builder): + return common_query_builder("""{ + items { + pValue + log10PValue + foldChange + log10FoldChange + numWildTypes + numMutants + dataSet { name } + feature { name } + mutation { + name + gene { + entrez + hgnc + } + mutationCode + mutationType { + name + display + } + } + tag { name } + } + }""") + + +def test_driverResults_cursor_pagination_first(client, paging_query): + num = 10 + response = client.post( + '/api', json={'query': paging_query, 'variables': { + 'paging': {'first': num} + }}) + json_data = json.loads(response.data) + page = json_data['data']['driverResults'] + items = page['items'] + paging = page['paging'] + start = from_cursor_hash(paging['startCursor']) + end = from_cursor_hash(paging['endCursor']) + + assert len(items) == num + assert paging['hasNextPage'] == True + assert paging['hasPreviousPage'] == False + assert start == items[0]['id'] + assert end == items[num - 1]['id'] + + +def test_driverResults_cursor_pagination_last(client, paging_query): + num = 10 + response = client.post( + '/api', json={'query': paging_query, 'variables': { + 'paging': { + 'last': num, + 'before': to_cursor_hash(1000) + } + }}) + json_data = json.loads(response.data) + page = json_data['data']['driverResults'] + items = page['items'] + paging = page['paging'] + start = from_cursor_hash(paging['startCursor']) + end = from_cursor_hash(paging['endCursor']) + + assert len(items) == num + assert paging['hasNextPage'] == False + assert paging['hasPreviousPage'] == True + assert start == items[0]['id'] + assert end == items[num - 1]['id'] + + +def test_driverResults_cursor_distinct_pagination(client, paging_query): + page_num = 2 + num = 10 + response = client.post( + '/api', json={'query': paging_query, 'variables': { + 'paging': { + 'page': page_num, + 'first': num, + }, + 'distinct': True, + 'dataSet': ['TCGA'], + 'tag': ['C1'] + }}) + json_data = json.loads(response.data) + page = json_data['data']['driverResults'] + items = page['items'] + + assert len(items) == num + assert page_num == page['paging']['page'] + + +def test_driverResults_query_with_no_arguments_no_relations(client, simple_query): + num = 10 + response = client.post( + '/api', + json={ + 'query': simple_query, + 'variables': {'paging': {'first': num}} + } + ) + json_data = json.loads(response.data) + page = json_data['data']['driverResults'] + driver_results = page['items'] + assert isinstance(driver_results, list) + assert len(driver_results) == num + for driver_result in driver_results[0:2]: + assert type(driver_result['foldChange']) is float or NoneType + assert type(driver_result['pValue']) is float or NoneType + assert type(driver_result['log10PValue']) is float or NoneType + assert type(driver_result['log10FoldChange']) is float or NoneType + assert type(driver_result['numWildTypes']) is int or NoneType + assert type(driver_result['numMutants']) is int or NoneType + + +def test_driverResults_query_with_passed_data_set_entrez_feature_and_tag(client, common_query, data_set, dr_feature, gene_entrez, dr_tag_name): + num = 1 + response = client.post('/api', json={'query': common_query, 'variables': { + 'dataSet': [data_set], + 'entrez': [gene_entrez], + 'feature': [dr_feature], + 'tag': [dr_tag_name], + 'paging': {'first': num} + }}) + json_data = json.loads(response.data) + page = json_data['data']['driverResults'] + results = page['items'] + assert isinstance(results, list) + assert len(results) == num + for result in results[0:2]: + assert result['dataSet']['name'] == data_set + assert result['feature']['name'] == dr_feature + assert result['tag']['name'] == dr_tag_name + assert type(result['mutation']['name']) is str + assert type(result['mutation']['gene']['entrez']) is int + assert result['mutation']['gene']['entrez'] == gene_entrez + assert type(result['mutation']['mutationCode']) is str + + +def test_driverResults_query_with_passed_data_set_entrez_feature_and_mutation(client, common_query, data_set, dr_feature, gene_entrez, mutation_code): + response = client.post('/api', json={'query': common_query, 'variables': { + 'dataSet': [data_set], + 'entrez': [gene_entrez], + 'feature': [dr_feature], + 'mutationCode': [mutation_code] + }}) + json_data = json.loads(response.data) + page = json_data['data']['driverResults'] + results = page['items'] + assert isinstance(results, list) + assert len(results) > 0 + for result in results[0:2]: + assert result['dataSet']['name'] == data_set + assert result['feature']['name'] == dr_feature + assert result['mutation']['gene']['entrez'] == gene_entrez + assert result['mutation']['mutationCode'] == mutation_code + assert type(result['tag']['name']) is str + + +def test_driverResults_query_with_passed_data_set_related_entrez_feature_and_mutation(client, common_query, data_set, dr_feature, gene_entrez, mutation_code, related): + response = client.post('/api', json={'query': common_query, 'variables': { + 'dataSet': [data_set], + 'entrez': [gene_entrez], + 'feature': [dr_feature], + 'mutationCode': [mutation_code], + 'related': ['does_not_exist'] + }}) + json_data = json.loads(response.data) + page = json_data['data']['driverResults'] + results = page['items'] + assert isinstance(results, list) + assert len(results) == 0 + + response = client.post('/api', json={'query': common_query, 'variables': { + 'dataSet': [data_set], + 'entrez': [gene_entrez], + 'feature': [dr_feature], + 'mutationCode': [mutation_code], + 'related': [related] + }}) + json_data = json.loads(response.data) + page = json_data['data']['driverResults'] + results = page['items'] + assert isinstance(results, list) + assert len(results) > 0 + for result in results[0:2]: + assert result['dataSet']['name'] == data_set + assert result['feature']['name'] == dr_feature + assert result['mutation']['gene']['entrez'] == gene_entrez + assert result['mutation']['mutationCode'] == mutation_code + assert type(result['tag']['name']) is str + + +def test_driverResults_query_with_passed_data_set_entrez_mutation_code_and_tag(client, common_query, data_set, gene_entrez, mutation_code, dr_tag_name): + response = client.post('/api', json={'query': common_query, 'variables': { + 'dataSet': [data_set], + 'entrez': [gene_entrez], + 'mutationCode': [mutation_code], + 'tag': [dr_tag_name] + }}) + json_data = json.loads(response.data) + page = json_data['data']['driverResults'] + results = page['items'] + assert isinstance(results, list) + assert len(results) > 0 + for result in results[0:2]: + assert result['dataSet']['name'] == data_set + assert type(result['feature']['name']) is str + assert result['mutation']['gene']['entrez'] == gene_entrez + assert result['mutation']['mutationCode'] == mutation_code + assert result['tag']['name'] == dr_tag_name + + +def test_driverResults_query_with_passed_data_set_feature_mutation_code_and_tag(client, common_query, data_set, dr_feature, mutation_code, dr_tag_name): + response = client.post('/api', json={'query': common_query, 'variables': { + 'dataSet': [data_set], + 'feature': [dr_feature], + 'mutationCode': [mutation_code], + 'tag': [dr_tag_name] + }}) + json_data = json.loads(response.data) + page = json_data['data']['driverResults'] + results = page['items'] + assert isinstance(results, list) + assert len(results) > 0 + for result in results[0:2]: + assert result['dataSet']['name'] == data_set + assert result['feature']['name'] == dr_feature + assert type(result['mutation']['gene']['entrez']) is int + assert result['mutation']['mutationCode'] == mutation_code + assert result['tag']['name'] == dr_tag_name + + +def test_driverResults_query_with_passed_data_set_feature_mutation_code_entrez_and_tag(client, common_query, data_set, dr_feature, mutation_code, dr_tag_name, gene_entrez, gene_hgnc, dr_mutation): + response = client.post('/api', json={'query': common_query, 'variables': { + 'dataSet': [data_set], + 'feature': [dr_feature], + 'mutationCode': [mutation_code], + 'tag': [dr_tag_name], + 'entrez': [gene_entrez], + }}) + json_data = json.loads(response.data) + page = json_data['data']['driverResults'] + results = page['items'] + assert isinstance(results, list) + assert len(results) == 1 + for result in results: + assert result['dataSet']['name'] == data_set + assert result['feature']['name'] == dr_feature + assert result['tag']['name'] == dr_tag_name + assert result['mutation']['name'] == dr_mutation + assert result['mutation']['gene']['entrez'] == gene_entrez + assert result['mutation']['gene']['hgnc'] == gene_hgnc + assert result['mutation']['mutationCode'] == mutation_code + assert result['mutation']['mutationType']['name'] == 'driver_mutation' + assert result['mutation']['mutationType']['display'] == 'Driver Mutation' + + +def test_driverResults_query_with_passed_data_set_feature_tag_and_mutation(client, common_query, data_set, dr_feature, dr_mutation, dr_tag_name, gene_entrez, gene_hgnc, mutation_code): + response = client.post('/api', json={'query': common_query, 'variables': { + 'dataSet': [data_set], + 'feature': [dr_feature], + 'mutation': [dr_mutation], + 'tag': [dr_tag_name] + }}) + json_data = json.loads(response.data) + page = json_data['data']['driverResults'] + results = page['items'] + assert isinstance(results, list) + assert len(results) == 1 + for result in results: + assert result['dataSet']['name'] == data_set + assert result['feature']['name'] == dr_feature + assert result['mutation']['name'] == dr_mutation + assert result['tag']['name'] == dr_tag_name + assert result['mutation']['gene']['entrez'] == gene_entrez + assert result['mutation']['gene']['hgnc'] == gene_hgnc + assert result['mutation']['mutationCode'] == mutation_code + assert result['mutation']['mutationType']['name'] == 'driver_mutation' + assert result['mutation']['mutationType']['display'] == 'Driver Mutation' + + +def test_driverResults_query_with_passed_data_set_entrez_feature_mutation_code_and_tag(client, common_query, dr_feature, gene_entrez, mutation_code, dr_tag_name): + response = client.post( + '/api', json={'query': common_query, 'variables': { + 'entrez': [gene_entrez], + 'feature': [dr_feature], + 'mutationCode': [mutation_code], + 'tag': [dr_tag_name] + }}) + json_data = json.loads(response.data) + page = json_data['data']['driverResults'] + results = page['items'] + assert isinstance(results, list) + assert len(results) > 0 + for result in results[0:2]: + assert type(result['dataSet']['name']) is str + assert result['feature']['name'] == dr_feature + assert result['mutation']['gene']['entrez'] == gene_entrez + assert result['mutation']['mutationCode'] == mutation_code + assert result['tag']['name'] == dr_tag_name + + +def test_driverResults_query_with_passed_min_p_value(client, common_query, data_set, gene_entrez, dr_feature, min_p_value, dr_tag_name): + response = client.post( + '/api', json={'query': common_query, 'variables': { + 'dataSet': [data_set], + 'entrez': [gene_entrez], + 'feature': [dr_feature], + 'minPValue': min_p_value, + 'tag': [dr_tag_name] + }}) + json_data = json.loads(response.data) + page = json_data['data']['driverResults'] + results = page['items'] + assert isinstance(results, list) + assert len(results) > 0 + for result in results[0:2]: + assert result['pValue'] >= min_p_value + + +def test_driverResults_query_with_passed_min_p_value_and_min_log10_p_value(client, common_query, data_set, gene_entrez, dr_feature, min_log10_p_value, min_p_value, dr_tag_name): + response = client.post( + '/api', json={'query': common_query, 'variables': { + 'dataSet': [data_set], + 'entrez': [gene_entrez], + 'feature': [dr_feature], + 'minLog10PValue': min_log10_p_value, + 'minPValue': min_p_value, + 'tag': [dr_tag_name] + }}) + json_data = json.loads(response.data) + page = json_data['data']['driverResults'] + results = page['items'] + assert isinstance(results, list) + assert len(results) > 0 + for result in results[0:2]: + assert result['pValue'] >= min_p_value + + +def test_driverResults_query_with_passed_min_log10_p_value(client, common_query, data_set, gene_entrez, dr_feature, min_log10_p_value, dr_tag_name): + response = client.post( + '/api', json={'query': common_query, 'variables': { + 'dataSet': [data_set], + 'entrez': [gene_entrez], + 'feature': [dr_feature], + 'minLog10PValue': min_log10_p_value, + 'tag': [dr_tag_name] + }}) + json_data = json.loads(response.data) + page = json_data['data']['driverResults'] + results = page['items'] + assert isinstance(results, list) + assert len(results) > 0 + for result in results[0:2]: + assert result['log10PValue'] >= min_log10_p_value + + +def test_driverResults_query_with_passed_max_p_value(client, common_query, data_set, gene_entrez, dr_feature, max_p_value, dr_tag_name): + response = client.post( + '/api', json={'query': common_query, 'variables': { + 'dataSet': [data_set], + 'entrez': [gene_entrez], + 'feature': [dr_feature], + 'maxPValue': max_p_value, + 'tag': [dr_tag_name] + }}) + json_data = json.loads(response.data) + page = json_data['data']['driverResults'] + results = page['items'] + assert isinstance(results, list) + assert len(results) > 0 + for result in results[0:2]: + assert result['pValue'] <= max_p_value + + +def test_driverResults_query_with_passed_max_p_value_and_max_log10_p_value(client, common_query, data_set, gene_entrez, dr_feature, max_log10_p_value, max_p_value, dr_tag_name): + response = client.post( + '/api', json={'query': common_query, 'variables': { + 'dataSet': [data_set], + 'entrez': [gene_entrez], + 'feature': [dr_feature], + 'maxLog10PValue': max_log10_p_value, + 'maxPValue': max_p_value, + 'tag': [dr_tag_name] + }}) + json_data = json.loads(response.data) + page = json_data['data']['driverResults'] + results = page['items'] + assert isinstance(results, list) + assert len(results) > 0 + for result in results[0:2]: + assert result['pValue'] <= max_p_value + + +def test_driverResults_query_with_passed_max_log10_p_value(client, common_query, data_set, gene_entrez, dr_feature, max_log10_p_value, dr_tag_name): + response = client.post( + '/api', json={'query': common_query, 'variables': { + 'dataSet': [data_set], + 'entrez': [gene_entrez], + 'feature': [dr_feature], + 'maxLog10PValue': max_log10_p_value, + 'tag': [dr_tag_name] + }}) + json_data = json.loads(response.data) + page = json_data['data']['driverResults'] + results = page['items'] + assert isinstance(results, list) + assert len(results) > 0 + for result in results[0:2]: + assert result['log10PValue'] <= max_log10_p_value + + +def test_driverResults_query_with_passed_min_fold_change(client, common_query, data_set, gene_entrez, dr_feature, min_fold_change, dr_tag_name): + response = client.post( + '/api', json={'query': common_query, 'variables': { + 'dataSet': [data_set], + 'entrez': [gene_entrez], + 'feature': [dr_feature], + 'minFoldChange': min_fold_change, + 'tag': [dr_tag_name] + }}) + json_data = json.loads(response.data) + page = json_data['data']['driverResults'] + results = page['items'] + assert isinstance(results, list) + assert len(results) > 0 + for result in results[0:2]: + assert result['foldChange'] >= min_fold_change + + +def test_driverResults_query_with_passed_min_fold_change_and_min_log10_fold_change(client, common_query, data_set, gene_entrez, dr_feature, min_log10_fold_change, min_fold_change, dr_tag_name): + response = client.post( + '/api', json={'query': common_query, 'variables': { + 'dataSet': [data_set], + 'entrez': [gene_entrez], + 'feature': [dr_feature], + 'maxLog10FoldChange': min_log10_fold_change, + 'minFoldChange': min_fold_change, + 'tag': [dr_tag_name] + }}) + json_data = json.loads(response.data) + page = json_data['data']['driverResults'] + results = page['items'] + assert isinstance(results, list) + assert len(results) > 0 + for result in results[0:2]: + assert result['foldChange'] >= min_fold_change + + +def test_driverResults_query_with_passed_min_log10_fold_change(client, common_query, data_set, gene_entrez, dr_feature, min_log10_fold_change, dr_tag_name): + response = client.post( + '/api', json={'query': common_query, 'variables': { + 'dataSet': [data_set], + 'entrez': [gene_entrez], + 'feature': [dr_feature], + 'minLog10FoldChange': min_log10_fold_change, + 'tag': [dr_tag_name] + }}) + json_data = json.loads(response.data) + page = json_data['data']['driverResults'] + results = page['items'] + assert isinstance(results, list) + assert len(results) > 0 + for result in results[0:2]: + assert result['log10FoldChange'] >= min_log10_fold_change + + +def test_driverResults_query_with_passed_min_n_mut(client, common_query, data_set, gene_entrez, dr_feature, min_n_mut, dr_tag_name): + response = client.post( + '/api', json={'query': common_query, 'variables': { + 'dataSet': [data_set], + 'entrez': [gene_entrez], + 'feature': [dr_feature], + 'minNumMutants': min_n_mut, + 'tag': [dr_tag_name] + }}) + json_data = json.loads(response.data) + page = json_data['data']['driverResults'] + results = page['items'] + assert isinstance(results, list) + assert len(results) > 0 + for result in results[0:2]: + assert result['numMutants'] >= min_n_mut + + +def test_driverResults_query_with_passed_min_n_wt(client, common_query, data_set, gene_entrez, dr_feature, min_n_wt, dr_tag_name): + response = client.post( + '/api', json={'query': common_query, 'variables': { + 'dataSet': [data_set], + 'entrez': [gene_entrez], + 'feature': [dr_feature], + 'minNumWildTypes': min_n_wt, + 'tag': [dr_tag_name] + }}) + json_data = json.loads(response.data) + page = json_data['data']['driverResults'] + results = page['items'] + assert isinstance(results, list) + assert len(results) > 0 + for result in results[0:2]: + assert result['numWildTypes'] >= min_n_wt diff --git a/apps/iatlas/api-gitlab/tests/queries/test_edges_query.py b/apps/iatlas/api-gitlab/tests/queries/test_edges_query.py new file mode 100644 index 0000000000..ee8856008a --- /dev/null +++ b/apps/iatlas/api-gitlab/tests/queries/test_edges_query.py @@ -0,0 +1,352 @@ +import json +import pytest +from tests import NoneType +from api.resolvers.resolver_helpers.paging_utils import from_cursor_hash, to_cursor_hash, Paging + + +@pytest.fixture(scope='module') +def max_score(): + return 0.6 + + +@pytest.fixture(scope='module') +def min_score(): + return 0.5 + + +@pytest.fixture(scope='module') +def node_1(): + return 'PCAWG_cellimage_network_BLCA-US_940' + + +@pytest.fixture(scope='module') +def node_2(): + return 'PCAWG_cellimage_network_BLCA-US_T_cells_CD8_Aggregate2' + + +@pytest.fixture(scope='module') +def node_3(): + return 'TCGA_extracellular_network_PRAD_3_ETV4_T_cells_CD8_Aggregate2' + + +@pytest.fixture(scope='module') +def common_query_builder(): + def f(query_fields): + return """query Edges( + $paging: PagingInput + $distinct: Boolean + $maxScore: Float + $minScore: Float + $node1: [String!] + $node2: [String!] + ) { + edges( + paging: $paging + distinct: $distinct + maxScore: $maxScore + minScore: $minScore + node1: $node1 + node2: $node2 + )""" + query_fields + "}" + return f + + +def test_edges_cursor_pagination_first(client, common_query_builder): + query = common_query_builder("""{ + paging { + startCursor + endCursor + hasNextPage + hasPreviousPage + } + items { id } + }""") + num = 10 + response = client.post( + '/api', json={'query': query, 'variables': { + 'paging': {'first': num} + }}) + json_data = json.loads(response.data) + page = json_data['data']['edges'] + items = page['items'] + paging = page['paging'] + start = from_cursor_hash(paging['startCursor']) + end = from_cursor_hash(paging['endCursor']) + + assert len(items) == num + assert paging['hasNextPage'] == True + assert paging['hasPreviousPage'] == False + + +def test_edges_cursor_pagination_last(client, common_query_builder): + query = common_query_builder("""{ + paging { + startCursor + endCursor + hasNextPage + hasPreviousPage + } + items { id } + }""") + num = 10 + response = client.post( + '/api', json={'query': query, 'variables': { + 'paging': { + 'last': num, + 'before': to_cursor_hash(100) + } + }}) + json_data = json.loads(response.data) + page = json_data['data']['edges'] + items = page['items'] + paging = page['paging'] + start = from_cursor_hash(paging['startCursor']) + end = from_cursor_hash(paging['endCursor']) + + assert len(items) == num + assert paging['hasNextPage'] == False + assert paging['hasPreviousPage'] == True + assert start == items[0]['id'] + assert end == items[num - 1]['id'] + + +def test_edges_cursor_distinct_pagination(client, common_query_builder): + query = common_query_builder("""{ + paging { + page + } + items { + name + node1 { name } + } + }""") + page_num = 2 + num = 10 + response = client.post( + '/api', json={'query': query, 'variables': { + 'paging': { + 'page': page_num, + 'first': num, + }, + 'distinct': True, + 'dataSet': ['TCGA'], + 'related': ['Immune_Subtype'] + }}) + json_data = json.loads(response.data) + page = json_data['data']['edges'] + items = page['items'] + + assert len(items) == num + assert page_num == page['paging']['page'] + + +def test_edges_missing_pagination(client, common_query_builder): + """Verify that query does not error when paging is not sent by the client + + The purpose of this test is the ensure that valid and sensible default values + are used and the query does not error, when no paging arguments are sent. + Cursor pagination and a limit of 100,000 will be used by default. + """ + query = common_query_builder("""{ + paging { + startCursor + endCursor + hasNextPage + hasPreviousPage + } + items { id } + }""") + response = client.post( + '/api', json={'query': query, 'variables': { + 'dataSet': ['TCGA'], + 'related': ['Immune_Subtype'] + }}) + json_data = json.loads(response.data) + page = json_data['data']['edges'] + items = page['items'] + + assert len(items) == Paging.MAX_LIMIT + + +def test_edges_query_with_passed_node1_and_node2(client, common_query_builder, node_1): + query = common_query_builder( + """{ + items { name } + paging { + page + pages + total + } + }""" + ) + response = client.post( + '/api', + json={ + 'query': query, + 'variables': { + 'paging': {'type': Paging.OFFSET}, + 'node1': ['PCAWG_extracellular_network_C2_8754'], + 'node2': ['PCAWG_extracellular_network_C2_3655'] + } + } + ) + json_data = json.loads(response.data) + page = json_data['data']['edges'] + results = page['items'] + paging = page['paging'] + + assert paging['page'] == 1 + assert type(paging['pages']) is int + assert type(paging['total']) is int + assert isinstance(results, list) + assert len(results) > 0 + for result in results[0:2]: + assert type(result['name']) is str + + +def test_edges_query_with_passed_node1(client, common_query_builder): + query = common_query_builder("""{ + items { + name + node1 { name } + } + }""") + response = client.post('/api', json={'query': query, + 'variables': {'node1': ['PCAWG_extracellular_network_C2_8754']}}) + json_data = json.loads(response.data) + page = json_data['data']['edges'] + results = page['items'] + + assert isinstance(results, list) + assert len(results) > 0 + for result in results[0:2]: + assert type(result['name']) is str + assert result['node1']['name'] == 'PCAWG_extracellular_network_C2_8754' + + +def test_edges_query_with_passed_node2(client, common_query_builder): + query = common_query_builder("""{ + items { + label + name + score + node1 { name } + node2 { name } + } + }""") + response = client.post( + '/api', + json={ + 'query': query, + 'variables': {'node2': ['PCAWG_extracellular_network_C2_3655']} + } + ) + json_data = json.loads(response.data) + page = json_data['data']['edges'] + results = page['items'] + + import logging + logging.warning(node_2) + assert isinstance(results, list) + assert len(results) > 0 + for result in results[0:2]: + assert type(result['label']) is str or NoneType + assert type(result['name']) is str + assert type(result['score']) is float or NoneType + assert type(result['node1']['name']) is str + assert result['node2']['name'] == 'PCAWG_extracellular_network_C2_3655' + + +def test_edges_query_with_passed_maxScore_and_node2(client, common_query_builder, max_score, node_3): + query = common_query_builder("""{ + items { + label + name + score + } + }""") + response = client.post('/api', json={'query': query, + 'variables': {'maxScore': max_score, 'node2': [node_3]}}) + json_data = json.loads(response.data) + page = json_data['data']['edges'] + results = page['items'] + + assert isinstance(results, list) + assert len(results) > 0 + for result in results[0:2]: + assert type(result['label']) is str or NoneType + assert type(result['name']) is str + assert result['score'] <= max_score + + +def test_edges_query_with_passed_minScore_and_node2(client, common_query_builder, min_score, node_3): + query = common_query_builder("""{ + items { + label + name + score + } + }""") + response = client.post('/api', json={'query': query, + 'variables': {'minScore': min_score, 'node2': [node_3]}}) + json_data = json.loads(response.data) + page = json_data['data']['edges'] + results = page['items'] + + assert isinstance(results, list) + assert len(results) > 0 + for result in results[0:2]: + assert type(result['label']) is str or NoneType + assert type(result['name']) is str + assert result['score'] >= min_score + + +def test_edges_query_with_passed_maxScore_minScore_and_node2(client, common_query_builder, max_score, min_score, node_3): + query = common_query_builder("""{ + items { + label + name + score + } + }""") + response = client.post('/api', json={'query': query, + 'variables': {'maxScore': max_score, + 'minScore': min_score, + 'node2': [node_3]}}) + json_data = json.loads(response.data) + page = json_data['data']['edges'] + results = page['items'] + + assert isinstance(results, list) + assert len(results) > 0 + for result in results[0:2]: + assert type(result['label']) is str or NoneType + assert type(result['name']) is str + assert result['score'] <= max_score + assert result['score'] >= min_score + + +def test_edges_query_with_no_arguments(client, common_query_builder): + query = common_query_builder("""{ + items { + name + node1 { name } + } + }""") + num = 1000 + response = client.post( + '/api', + json={ + 'query': query, + 'variables': {'paging': {'first': num}} + } + ) + json_data = json.loads(response.data) + page = json_data['data']['edges'] + results = page['items'] + + assert isinstance(results, list) + assert len(results) > 0 + for result in results[0:2]: + assert type(result['name']) is str + assert type(result['node1']['name']) is str diff --git a/apps/iatlas/api-gitlab/tests/queries/test_features_query.py b/apps/iatlas/api-gitlab/tests/queries/test_features_query.py new file mode 100644 index 0000000000..f9aa2dbfcf --- /dev/null +++ b/apps/iatlas/api-gitlab/tests/queries/test_features_query.py @@ -0,0 +1,752 @@ +import json +import pytest +from tests import NoneType +from api.enums import unit_enum +from api.database import return_feature_query +from api.resolvers.resolver_helpers.paging_utils import from_cursor_hash, to_cursor_hash, Paging +from decimal import Decimal + + +@pytest.fixture(scope='module') +def feature_name(): + return 'BCR_Richness' + +@pytest.fixture(scope='module') +def feature_class(): + return 'Adaptive Receptor - B cell' + +@pytest.fixture(scope='module') +def max_value(): + return 2000 + + +@pytest.fixture(scope='module') +def min_value(): + return 1000 + + +@pytest.fixture(scope='module') +def common_query_builder(): + def f(query_fields): + return """ + query Features($feature: [String!] + $featureClass: [String!] + $cohort: [String!] + $sample: [String!] + $minValue: Float + $maxValue: Float + $paging: PagingInput + $distinct: Boolean + ) { + features( + feature: $feature + featureClass: $featureClass + cohort: $cohort + sample: $sample + minValue: $minValue + maxValue: $maxValue + paging: $paging + distinct: $distinct + ) + """ + query_fields + "}" + return f + + +@pytest.fixture(scope='module') +def common_query(common_query_builder): + return common_query_builder( + """ + { + items { + id + class + display + name + order + unit + methodTag + germlineModule + germlineCategory + } + paging { + type + pages + total + startCursor + endCursor + hasPreviousPage + hasNextPage + page + limit + } + error + } + """ + ) + + +@pytest.fixture(scope='module') +def samples_query(common_query_builder): + return common_query_builder( + """ + { + items { + id + name + class + order + samples { + name + value + } + } + paging { + type + pages + total + startCursor + endCursor + hasPreviousPage + hasNextPage + page + limit + } + error + } + """ + ) + + +@pytest.fixture(scope='module') +def pseudobulk_query(common_query_builder): + return common_query_builder( + """ + { + items { + id + name + class + order + pseudoBulkSamples { + name + value + cellType + } + } + paging { + type + pages + total + startCursor + endCursor + hasPreviousPage + hasNextPage + page + limit + } + error + } + """ + ) + + +@pytest.fixture(scope='module') +def cells_query(common_query_builder): + return common_query_builder( + """ + { + items { + id + name + class + order + cells { + name + type + value + } + } + paging { + type + pages + total + startCursor + endCursor + hasPreviousPage + hasNextPage + page + limit + } + error + } + """ + ) + +@pytest.fixture(scope='module') +def values_query(common_query_builder): + return common_query_builder( + """ + { + items { + name + class + valueMin + valueMax + } + paging { + type + pages + total + startCursor + endCursor + hasPreviousPage + hasNextPage + page + limit + } + error + } + """ + ) + + +def test_features_cursor_pagination_first(client, common_query_builder): + query = common_query_builder("""{ + items { + id + } + paging { + type + pages + total + startCursor + endCursor + hasPreviousPage + hasNextPage + page + limit + } + }""") + num = 5 + response = client.post( + '/api', json={'query': query, 'variables': { + 'paging': {'first': num} + }}) + json_data = json.loads(response.data) + page = json_data['data']['features'] + items = page['items'] + paging = page['paging'] + start = from_cursor_hash(paging['startCursor']) + end = from_cursor_hash(paging['endCursor']) + + assert len(items) == num + assert paging['hasNextPage'] == True + assert paging['hasPreviousPage'] == False + + +def test_features_cursor_pagination_last(client, common_query_builder): + query = common_query_builder("""{ + items { + id + } + paging { + type + pages + total + startCursor + endCursor + hasPreviousPage + hasNextPage + page + limit + } + }""") + num = 5 + response = client.post( + '/api', json={'query': query, 'variables': { + 'paging': { + 'last': num, + 'before': to_cursor_hash(1000) + } + }}) + json_data = json.loads(response.data) + page = json_data['data']['features'] + items = page['items'] + paging = page['paging'] + start = from_cursor_hash(paging['startCursor']) + end = from_cursor_hash(paging['endCursor']) + + assert len(items) == num + assert paging['hasNextPage'] == False + assert paging['hasPreviousPage'] == True + + +def test_features_cursor_distinct_pagination(client, common_query): + page_num = 2 + num = 2 + response = client.post( + '/api', json={'query': common_query, 'variables': { + 'paging': { + 'page': page_num, + 'first': num, + }, + 'distinct': True, + }}) + json_data = json.loads(response.data) + page = json_data['data']['features'] + items = page['items'] + + assert len(items) == num + assert page_num == page['paging']['page'] + + +def test_features_query_with_no_args(client, common_query): + response = client.post('/api', json={'query': common_query}) + json_data = json.loads(response.data) + page = json_data['data']['features'] + features = page['items'] + + # Get the total number of features in the database. + feature_count = return_feature_query('id').count() + + assert isinstance(features, list) + assert len(features) == feature_count + for feature in features[0:3]: + assert type(feature['name']) is str + assert type(feature['display']) is str + assert type(feature['class']) is str + assert type(feature['methodTag']) is str or NoneType + assert type(feature['order']) is int or NoneType + assert feature['unit'] in unit_enum.enums or type( + feature['unit']) is NoneType + assert type(feature['germlineModule']) is str or NoneType + assert type(feature['germlineCategory']) is str or NoneType + + +def test_features_query_with_feature(client, feature_name, common_query): + response = client.post( + '/api', json={ + 'query': common_query, + 'variables': {'feature': [feature_name]} + } + ) + json_data = json.loads(response.data) + page = json_data['data']['features'] + features = page['items'] + + assert isinstance(features, list) + assert len(features) == 1 + feature = features[0] + assert feature['name'] == feature_name + assert type(feature['display']) is str + assert type(feature['class']) is str + assert type(feature['methodTag']) is str or NoneType + assert type(feature['order']) is int or NoneType + assert feature['unit'] in unit_enum.enums or type( + feature['unit']) is NoneType + assert type(feature['germlineModule']) is str or NoneType + assert type(feature['germlineCategory']) is str or NoneType + +def test_features_query_with_cohort(client, common_query): + response = client.post( + '/api', json={ + 'query': common_query, + 'variables': { + 'cohort': ['Bi_2021', 'Krishna_2021', 'Li_2022'] + } + } + ) + json_data = json.loads(response.data) + page = json_data['data']['features'] + features = page['items'] + + assert isinstance(features, list) + assert len(features) > 0 + feature = features[0] + assert type(feature['name']) is str + assert type(feature['display']) is str + assert type(feature['class']) is str + assert type(feature['methodTag']) is str or NoneType + assert type(feature['order']) is int or NoneType + assert feature['unit'] in unit_enum.enums or type( + feature['unit']) is NoneType + assert type(feature['germlineModule']) is str or NoneType + assert type(feature['germlineCategory']) is str or NoneType + + +def test_features_query_with_feature_class(client, feature_class, common_query): + response = client.post( + '/api', json={ + 'query': common_query, + 'variables': {'featureClass': [feature_class]} + } + ) + json_data = json.loads(response.data) + page = json_data['data']['features'] + features = page['items'] + + assert isinstance(features, list) + assert len(features) > 1 + for feature in features: + assert type(feature['name']) is str + assert type(feature['display']) is str + assert feature['class'] == feature_class + assert type(feature['methodTag']) is str or NoneType + assert type(feature['order']) is int or NoneType + assert feature['unit'] in unit_enum.enums or type( + feature['unit']) is NoneType + assert type(feature['germlineModule']) is str or NoneType + assert type(feature['germlineCategory']) is str or NoneType + + +def test_features_query_with_feature_and_feature_class(client, feature_name, feature_class, common_query): + response = client.post( + '/api', json={ + 'query': common_query, + 'variables': { + 'feature': [feature_name], + 'featureClass': [feature_class] + } + } + ) + json_data = json.loads(response.data) + page = json_data['data']['features'] + features = page['items'] + + assert isinstance(features, list) + assert len(features) == 1 + feature = features[0] + assert feature['name'] == feature_name + assert type(feature['display']) is str + assert feature['class'] == feature_class + assert type(feature['methodTag']) is str or NoneType + assert type(feature['order']) is int or NoneType + assert feature['unit'] in unit_enum.enums or type( + feature['unit']) is NoneType + assert type(feature['germlineModule']) is str or NoneType + assert type(feature['germlineCategory']) is str or NoneType + + +def test_features_query_with_passed_max_value(client, feature_name, max_value, values_query): + response = client.post( + '/api', json={'query': values_query, + 'variables': { + 'feature': [feature_name], + 'maxValue': max_value + }}) + json_data = json.loads(response.data) + page = json_data['data']['features'] + features = page['items'] + + assert isinstance(features, list) + assert len(features) == 1 + feature = features[0] + assert feature['name'] == feature_name + assert feature['valueMax'] <= max_value + + +def test_features_query_with_passed_min_value(client, feature_name, min_value, values_query): + response = client.post( + '/api', json={'query': values_query, + 'variables': { + 'feature': [feature_name], + 'minValue': min_value + }}) + json_data = json.loads(response.data) + page = json_data['data']['features'] + features = page['items'] + + assert isinstance(features, list) + assert len(features) == 1 + feature = features[0] + assert feature['name'] == feature_name + assert feature['valueMax'] >= min_value + + +def test_features_query_with_passed_max_value_and_class(client, feature_class, max_value, values_query): + response = client.post( + '/api', json={'query': values_query, + 'variables': { + 'featureClass': [feature_class], + 'maxValue': max_value + }}) + json_data = json.loads(response.data) + page = json_data['data']['features'] + features = page['items'] + + assert isinstance(features, list) + assert len(features) == 3 + for feature in features: + assert feature['class'] == feature_class + assert feature['valueMax'] <= max_value + + +def test_features_query_with_passed_min_value_and_class(client, feature_class, min_value, values_query): + response = client.post( + '/api', json={'query': values_query, + 'variables': { + 'featureClass': [feature_class], + 'minValue': min_value + }}) + json_data = json.loads(response.data) + page = json_data['data']['features'] + features = page['items'] + + assert isinstance(features, list) + assert len(features) == 1 + for feature in features: + assert feature['class'] == feature_class + assert feature['valueMax'] >= min_value + + +def test_features_query_values(client, feature_name, min_value, values_query): + response = client.post( + '/api', json={'query': values_query, + 'variables': { + 'feature': [feature_name], + 'minValue': min_value + }}) + json_data = json.loads(response.data) + page = json_data['data']['features'] + features = page['items'] + + assert isinstance(features, list) + assert len(features) == 1 + for feature in features: + assert feature['name'] == feature_name + assert feature['valueMax'] >= feature['valueMin'] + + +def test_feature_samples_query_with_feature(client, feature_name, samples_query): + response = client.post( + '/api', json={ + 'query': samples_query, + 'variables': {'feature': [feature_name]} + }) + json_data = json.loads(response.data) + page = json_data['data']['features'] + features = page['items'] + + assert isinstance(features, list) + assert len(features) == 1 + feature = features[0] + samples = feature['samples'] + assert feature['name'] == feature_name + assert type(feature['class']) is str + assert isinstance(samples, list) + assert len(samples) > 0 + for sample in samples[0:2]: + assert type(sample['name']) is str + assert type(sample['value']) is float + + +def test_feature_samples_query_with_class(client, feature_class, samples_query): + response = client.post( + '/api', json={ + 'query': samples_query, + 'variables': {'featureClass': [feature_class]} + }) + json_data = json.loads(response.data) + page = json_data['data']['features'] + features = page['items'] + + assert isinstance(features, list) + assert len(features) == 3 + for feature in features: + samples = feature['samples'] + assert feature['class'] == feature_class + assert type(feature['name']) is str + assert isinstance(samples, list) + assert len(samples) > 0 + for sample in samples[0:2]: + assert type(sample['name']) is str + assert type(sample['value']) is float + + +def test_feature_samples_query_with_feature_and_cohort(client, feature_name, samples_query, tcga_tag_cohort_name, tcga_tag_cohort_samples): + response = client.post( + '/api', json={ + 'query': samples_query, + 'variables': { + 'feature': [feature_name], + 'cohort': [tcga_tag_cohort_name] + } + }) + json_data = json.loads(response.data) + page = json_data['data']['features'] + features = page['items'] + assert isinstance(features, list) + assert len(features) == 1 + feature = features[0] + samples = feature['samples'] + assert feature['name'] == feature_name + assert type(feature['class']) is str + assert isinstance(samples, list) + assert len(samples) > 0 + for sample in samples[0:2]: + assert type(sample['name']) is str + assert type(sample['value']) is float + assert sample['name'] in tcga_tag_cohort_samples + + +def test_feature_samples_query_with_feature_and_cohort2(client, feature_name, feature_class, tcga_tag_cohort_name, tcga_tag_cohort_samples, samples_query): + response = client.post( + '/api', json={ + 'query': samples_query, + 'variables': { + 'feature': [feature_name], + 'cohort': [tcga_tag_cohort_name] + } + }) + json_data = json.loads(response.data) + page = json_data['data']['features'] + features = page['items'] + assert isinstance(features, list) + assert len(features) == 1 + feature = features[0] + samples = feature['samples'] + assert feature['name'] == feature_name + assert feature['class'] == feature_class + assert isinstance(samples, list) + assert len(samples) > 0 + for sample in samples[0:2]: + assert type(sample['name']) is str + assert type(sample['value']) is float + assert sample['name'] in tcga_tag_cohort_samples + +def test_feature_samples_query_with_feature_and_sample(client, feature_name, samples_query, sample): + response = client.post( + '/api', json={ + 'query': samples_query, + 'variables': { + 'feature': [feature_name], + 'sample': [sample] + } + }) + json_data = json.loads(response.data) + page = json_data['data']['features'] + features = page['items'] + assert isinstance(features, list) + assert len(features) == 1 + feature = features[0] + samples = feature['samples'] + assert feature['name'] == feature_name + assert type(feature['class']) is str + assert isinstance(samples, list) + assert len(samples) == 1 + for s in samples: + assert s['name'] == sample + assert type(s['value']) is float + + +def test_pseudobulk_query_with_feature(client, pseudobulk_query): + response = client.post( + '/api', json={ + 'query': pseudobulk_query, + 'variables': { + 'feature': ["Th1_cells"], + } + }) + json_data = json.loads(response.data) + page = json_data['data']['features'] + features = page['items'] + assert isinstance(features, list) + assert len(features) == 1 + feature = features[0] + assert feature['name'] == "Th1_cells" + assert isinstance(feature['class'], str) + samples = feature['pseudoBulkSamples'] + assert isinstance(samples, list) + assert len(samples) > 0 + for sample in samples[0:10]: + assert isinstance(sample['cellType'], str) + assert isinstance(sample['name'], str) + assert isinstance(sample['value'], float) + + +def test_pseudobulk_query_with_cohort(client, pseudobulk_query): + response = client.post( + '/api', json={ + 'query': pseudobulk_query, + 'variables': { + 'cohort': ['MSK_Biopsy_Site'] + } + }) + json_data = json.loads(response.data) + page = json_data['data']['features'] + features = page['items'] + assert isinstance(features, list) + assert len(features) > 0 + feature = features[0] + assert isinstance(feature['name'], str) + assert isinstance(feature['class'], str) + samples = feature['pseudoBulkSamples'] + assert isinstance(samples, list) + assert len(samples) > 0 + for sample in samples[0:10]: + assert isinstance(sample['cellType'], str) + assert isinstance(sample['name'], str) + assert isinstance(sample['value'], float) + + +def test_features_query_with_germline_feature(client, common_query, germline_feature, germline_module, germline_category): + response = client.post( + '/api', json={ + 'query': common_query, + 'variables': { + 'feature': [germline_feature] + } + }) + json_data = json.loads(response.data) + page = json_data['data']['features'] + features = page['items'] + assert isinstance(features, list) + assert len(features) == 1 + feature = features[0] + assert feature['name'] == germline_feature + assert feature['germlineModule'] == germline_module + assert feature['germlineCategory'] == germline_category + + +def test_feature_samples_query_with_class_and_cohort(client, samples_query, feature_class2, feature_class2_feature_names, tcga_tag_cohort_name, tcga_tag_cohort_samples): + + + response = client.post( + '/api', json={ + 'query': samples_query, + 'variables': { + 'featureClass': [feature_class2], + 'cohort': [tcga_tag_cohort_name] + } + }) + json_data = json.loads(response.data) + page = json_data['data']['features'] + features = page['items'] + assert isinstance(features, list) + assert len(features) == 10 + feature = features[0] + samples = feature['samples'] + assert feature['name'] in feature_class2_feature_names + assert feature['class'] == feature_class2 + assert isinstance(samples, list) + assert len(samples) > 0 + for sample in samples[0:2]: + assert type(sample['name']) is str + assert type(sample['value']) is float + assert sample['name'] in tcga_tag_cohort_samples + + +def test_feature_samples_query_with_class_and_pcawg_cohort(client, samples_query, feature_class2, pcawg_cohort_name): + response = client.post( + '/api', json={ + 'query': samples_query, + 'variables': { + 'featureClass': [feature_class2], + 'cohort': [pcawg_cohort_name] + } + }) + json_data = json.loads(response.data) + page = json_data['data']['features'] + features = page['items'] + assert isinstance(features, list) + assert len(features) == 0 diff --git a/apps/iatlas/api-gitlab/tests/queries/test_gene_types_query.py b/apps/iatlas/api-gitlab/tests/queries/test_gene_types_query.py new file mode 100644 index 0000000000..577604823f --- /dev/null +++ b/apps/iatlas/api-gitlab/tests/queries/test_gene_types_query.py @@ -0,0 +1,66 @@ +import json +import pytest +from api.database import return_gene_set_query +from tests import NoneType + + +@pytest.fixture(scope='module') +def gene_type(): + return 'CD8_CD68_ratio' + + +def test_gene_types_query_with_passed_gene_type(client, gene_type): + query = """query GeneTypes($name: [String!]) { + geneTypes(name: $name) { + display + genes { entrez, hgnc } + name + } + }""" + response = client.post( + '/api', json={'query': query, 'variables': {'name': [gene_type]}}) + json_data = json.loads(response.data) + results = json_data['data']['geneTypes'] + + assert isinstance(results, list) + assert len(results) == 1 + for result in results: + genes = result['genes'] + assert result['name'] == gene_type + assert type(result['display']) is str or NoneType + assert isinstance(genes, list) + assert len(genes) > 0 + for gene in genes[0:2]: + assert type(gene['entrez']) is int + + +def test_gene_types_query_with_passed_gene_type_no_genes(client, gene_type): + query = """query GeneTypes($name: [String!]) { + geneTypes(name: $name) { name } + }""" + response = client.post( + '/api', json={'query': query, 'variables': {'name': [gene_type]}}) + json_data = json.loads(response.data) + results = json_data['data']['geneTypes'] + + assert isinstance(results, list) + assert len(results) == 1 + for result in results[0:2]: + assert result['name'] == gene_type + + +def test_gene_types_query_no_args(client): + query = """query GeneTypes($name: [String!]) { + geneTypes(name: $name) { name } + }""" + response = client.post( + '/api', json={'query': query}) + json_data = json.loads(response.data) + results = json_data['data']['geneTypes'] + + gene_type_count = return_gene_set_query('id').count() + + assert isinstance(results, list) + assert len(results) == gene_type_count + for result in results[0:1]: + assert type(result['name']) is str diff --git a/apps/iatlas/api-gitlab/tests/queries/test_genes_query.py b/apps/iatlas/api-gitlab/tests/queries/test_genes_query.py new file mode 100644 index 0000000000..bcc7b75290 --- /dev/null +++ b/apps/iatlas/api-gitlab/tests/queries/test_genes_query.py @@ -0,0 +1,645 @@ +import json +import pytest +from api.resolvers.resolver_helpers.paging_utils import from_cursor_hash, to_cursor_hash +from tests import NoneType + +@pytest.fixture(scope='module') +def gene_type(): + return 'immunomodulator' + +@pytest.fixture(scope='module') +def gene_type2(): + return 'io_target' + +@pytest.fixture(scope='module') +def max_rna_seq_expr_1(): + return -0.727993495057642991952 + +@pytest.fixture(scope='module') +def min_rna_seq_expr_1(): + return 3424420 + +@pytest.fixture(scope='module') +def max_rna_seq_expr_2(): + return -0.377686024337191006417 + +@pytest.fixture(scope='module') +def min_rna_seq_expr_2(): + return -0.379707089801648023375 + +@pytest.fixture(scope='module') +def sample_name(): + return 'TCGA-27-1837' + +@pytest.fixture(scope='module') +def nanostring_sample(): + return "Prat_CanRes_2017-A42-ar-A42_pre" + +@pytest.fixture(scope='module') +def nanostring_entrez_id(): + return 135 + + +@pytest.fixture(scope='module') +def common_query_builder(): + def f(query_fields): + return """query Genes( + $entrez: [Int!] + $geneType: [String!] + $maxRnaSeqExpr: Float + $minRnaSeqExpr: Float + $cohort: [String!] + $sample: [String!] + $paging: PagingInput + $distinct: Boolean + ) { + genes( + paging: $paging + distinct: $distinct + cohort: $cohort + entrez: $entrez + geneType: $geneType + maxRnaSeqExpr: $maxRnaSeqExpr + minRnaSeqExpr: $minRnaSeqExpr + sample: $sample + )""" + query_fields + "}" + return f + + +@pytest.fixture(scope='module') +def common_query(common_query_builder): + return common_query_builder(""" + { + items { + entrez + hgnc + geneFamily + geneFunction + geneTypes { + name + display + } + immuneCheckpoint + pathway + publications { + firstAuthorLastName + journal + pubmedId + title + year + } + superCategory + therapyType + } + paging { + type + pages + total + startCursor + endCursor + hasPreviousPage + hasNextPage + page + limit + } + error + }""" + ) + + +@pytest.fixture(scope='module') +def rnaseq_query(common_query_builder): + return common_query_builder( + """ + { + items{ + entrez + hgnc + samples { + rnaSeqExpr + name + } + } + } + """ + ) + + +@pytest.fixture(scope='module') +def nanostring_query(common_query_builder): + return common_query_builder( + """ + { + items{ + entrez + samples { + nanostringExpr + name + } + } + } + """ + ) + + +@pytest.fixture(scope='module') +def pseudobulk_query(common_query_builder): + return common_query_builder( + """ + { + items{ + entrez + pseudoBulkSamples { + name + singleCellSeqSum + cellType + } + } + } + """ + ) + +@pytest.fixture(scope='module') +def cells_query(common_query_builder): + return common_query_builder( + """ + { + items{ + entrez + cells { + name + type + singleCellSeq + } + } + } + """ + ) + + +def test_cursor_pagination_first_without_samples(client, common_query_builder): + query = common_query_builder("""{ + items { + id + } + paging { + type + pages + total + startCursor + endCursor + hasPreviousPage + hasNextPage + page + limit + } + }""") + requested_n = 15 + response = client.post( + '/api', json={'query': query, 'variables': { + 'paging': {'first': requested_n} + }}) + json_data = json.loads(response.data) + page = json_data['data']['genes'] + items = page['items'] + paging = page['paging'] + start = from_cursor_hash(paging['startCursor']) + end = from_cursor_hash(paging['endCursor']) + + assert len(items) == requested_n + assert paging['hasNextPage'] == True + assert paging['hasPreviousPage'] == False + assert start == items[0]['id'] + assert end == items[requested_n - 1]['id'] + + +def test_cursor_pagination_first_with_samples(client, common_query_builder): + query = common_query_builder("""{ + items { + id + samples { name } + } + paging { + type + pages + total + startCursor + endCursor + hasPreviousPage + hasNextPage + page + limit + } + }""") + requested_n = 15 + max_n = 10 + response = client.post( + '/api', json={'query': query, 'variables': { + 'paging': {'first': requested_n} + }}) + json_data = json.loads(response.data) + page = json_data['data']['genes'] + items = page['items'] + paging = page['paging'] + start = from_cursor_hash(paging['startCursor']) + end = from_cursor_hash(paging['endCursor']) + + assert len(items) == max_n + assert paging['hasNextPage'] == True + assert paging['hasPreviousPage'] == False + assert start == items[0]['id'] + assert end == items[max_n - 1]['id'] + + +def test_cursor_pagination_last(client, common_query_builder): + query = common_query_builder("""{ + items { + id + } + paging { + type + pages + total + startCursor + endCursor + hasPreviousPage + hasNextPage + page + limit + } + }""") + num = 10 + response = client.post( + '/api', json={'query': query, 'variables': { + 'paging': { + 'last': num, + 'before': to_cursor_hash(1000) + } + }}) + json_data = json.loads(response.data) + page = json_data['data']['genes'] + items = page['items'] + paging = page['paging'] + start = from_cursor_hash(paging['startCursor']) + end = from_cursor_hash(paging['endCursor']) + + assert len(items) == num + assert paging['hasNextPage'] == False + assert paging['hasPreviousPage'] == True + assert start == items[0]['id'] + assert end == items[num - 1]['id'] + + +def test_cursor_distinct_pagination(client, common_query): + page_num = 2 + num = 10 + response = client.post( + '/api', json={'query': common_query, 'variables': { + 'paging': { + 'page': page_num, + 'first': num, + }, + 'distinct': True + }}) + json_data = json.loads(response.data) + page = json_data['data']['genes'] + items = page['items'] + + assert len(items) == num + assert page_num == page['paging']['page'] + + +def test_genes_query_with_entrez(client, common_query, entrez_id, hgnc_id): + response = client.post( + '/api', json={'query': common_query, 'variables': {'entrez': [entrez_id]}}) + json_data = json.loads(response.data) + page = json_data['data']['genes'] + results = page['items'] + + assert isinstance(results, list) + assert len(results) == 1 + for result in results: + gene_types = result['geneTypes'] + publications = result['publications'] + + assert result['entrez'] == entrez_id + assert result['hgnc'] == hgnc_id + assert type(result['geneFamily']) is str or NoneType + assert type(result['geneFunction']) is str or NoneType + assert isinstance(gene_types, list) + if gene_types: + for gene_type in gene_types: + assert type(gene_type['name']) is str + assert type(gene_type['display']) is str or NoneType + assert type(result['immuneCheckpoint']) is str or NoneType + assert type(result['pathway']) is str or NoneType + assert isinstance(publications, list) + if publications: + for publication in publications: + assert type( + publication['firstAuthorLastName']) is str or NoneType + assert type(publication['journal']) is str or NoneType + assert type(publication['pubmedId']) is int + assert type(publication['title']) is str or NoneType + assert type(publication['year']) is str or NoneType + assert type(result['superCategory']) is str or NoneType + assert type(result['therapyType']) is str or NoneType + + +def test_genes_query_with_gene_type(client, common_query, entrez_id, gene_type): + response = client.post( + '/api', json={'query': common_query, 'variables': {'entrez': [entrez_id], 'geneType': [gene_type]}}) + json_data = json.loads(response.data) + page = json_data['data']['genes'] + results = page['items'] + + assert isinstance(results, list) + assert len(results) == 1 + for result in results: + gene_types = result['geneTypes'] + + assert result['entrez'] == entrez_id + assert isinstance(gene_types, list) + for current_gene_type in gene_types: + assert current_gene_type['name'] == gene_type + + +def test_genes_query_with_gene_type2(client, common_query, entrez_id, gene_type2): + response = client.post( + '/api', json={'query': common_query, 'variables': {'entrez': [55], 'geneType': [gene_type2]}}) + json_data = json.loads(response.data) + page = json_data['data']['genes'] + results = page['items'] + + assert isinstance(results, list) + assert len(results) == 1 + for result in results: + assert result['pathway'] == 'Innate Immune System' + assert result['therapyType'] == 'Targeted by Other Immuno-Oncology Therapy Type' + + gene_types = result['geneTypes'] + + assert isinstance(gene_types, list) + for current_gene_type in gene_types: + assert current_gene_type['name'] == gene_type2 + + +def test_genes_query_no_entrez(client, common_query_builder): + query = common_query_builder( + """ + { + items{ + entrez + hgnc + } + } + """ + ) + response = client.post('/api', json={'query': query}) + json_data = json.loads(response.data) + page = json_data['data']['genes'] + results = page['items'] + + assert isinstance(results, list) + assert len(results) > 10 + for gene in results[0:1]: + assert type(gene['entrez']) is int + assert type(gene['hgnc']) is str + + +def test_genes_query_returns_publications(client, common_query_builder, entrez_id, hgnc_id): + query = common_query_builder( + """ + { + items{ + entrez + hgnc + publications { pubmedId } + } + } + """ + ) + response = client.post( + '/api', json={'query': query, 'variables': {'entrez': [entrez_id]}}) + json_data = json.loads(response.data) + page = json_data['data']['genes'] + results = page['items'] + + assert isinstance(results, list) + assert len(results) == 1 + for result in results: + publications = result['publications'] + + assert result['entrez'] == entrez_id + assert result['hgnc'] == hgnc_id + assert isinstance(publications, list) + assert len(publications) > 0 + for publication in publications[0:5]: + assert type(publication['pubmedId']) is int + + +def test_genes_query_returns_publications_with_geneType(client, common_query_builder, entrez_id, gene_type, hgnc_id): + query = common_query_builder( + """ + { + items{ + entrez + hgnc + publications { pubmedId } + } + } + """ + ) + response = client.post( + '/api', json={'query': query, 'variables': {'entrez': [entrez_id], 'geneType': [gene_type]}}) + json_data = json.loads(response.data) + page = json_data['data']['genes'] + results = page['items'] + + assert isinstance(results, list) + assert len(results) > 0 + for result in results: + publications = result['publications'] + + assert result['entrez'] == entrez_id + assert result['hgnc'] == hgnc_id + assert isinstance(publications, list) + assert len(publications) > 0 + for publication in publications[0:5]: + assert type(publication['pubmedId']) is int + + +def test_genes_rnaseq_query_with_gene_and_cohort(client, entrez_id, rnaseq_query, tcga_tag_cohort_name, tcga_tag_cohort_samples): + response = client.post( + '/api', json={ + 'query': rnaseq_query, + 'variables': { + 'entrez': [entrez_id], + 'cohort': [tcga_tag_cohort_name] + } + }) + json_data = json.loads(response.data) + page = json_data['data']['genes'] + genes = page['items'] + assert isinstance(genes, list) + assert len(genes) == 1 + gene = genes[0] + assert gene['entrez'] == entrez_id + samples = gene['samples'] + assert isinstance(samples, list) + assert len(samples) > 1 + for sample in gene['samples'][0:10]: + assert type(sample['name']) is str + assert type(sample['rnaSeqExpr']) is float + assert sample['name'] in tcga_tag_cohort_samples + + +def test_genes_rnaseq_query_with_gene_and_sample(client, entrez_id, rnaseq_query, sample): + response = client.post( + '/api', json={ + 'query': rnaseq_query, + 'variables': { + 'entrez': [entrez_id], + 'sample': [sample] + } + }) + json_data = json.loads(response.data) + page = json_data['data']['genes'] + genes = page['items'] + assert isinstance(genes, list) + assert len(genes) == 1 + gene = genes[0] + assert gene['entrez'] == entrez_id + samples = gene['samples'] + assert isinstance(samples, list) + assert len(samples) == 1 + s = samples[0] + assert type(s['name']) is str + assert type(s['rnaSeqExpr']) is float + assert s['name'] == sample + + +def test_genes_query_with_entrez_and_maxRnaSeqExpr(client, rnaseq_query, entrez_id): + max_rna_seq_expr = 1 + response = client.post( + '/api', json={ + 'query': rnaseq_query, + 'variables': { + 'maxRnaSeqExpr': max_rna_seq_expr, + 'entrez': entrez_id + } + }) + json_data = json.loads(response.data) + page = json_data['data']['genes'] + genes = page['items'] + assert isinstance(genes, list) + assert len(genes) == 1 + gene = genes[0] + assert gene['entrez'] == entrez_id + samples = gene['samples'] + assert isinstance(samples, list) + assert len(samples) > 1 + for sample in gene['samples'][0:10]: + assert type(sample['name']) is str + assert type(sample['rnaSeqExpr']) is float + assert sample['rnaSeqExpr'] <= max_rna_seq_expr + + +def test_genes_query_with_entrez_and_minRnaSeqExpr(client, rnaseq_query, entrez_id): + min_rna_seq_expr = 1 + response = client.post( + '/api', json={ + 'query': rnaseq_query, + 'variables': { + 'minRnaSeqExpr': min_rna_seq_expr, + 'entrez': entrez_id + } + }) + json_data = json.loads(response.data) + page = json_data['data']['genes'] + genes = page['items'] + assert isinstance(genes, list) + assert len(genes) == 1 + gene = genes[0] + assert gene['entrez'] == entrez_id + samples = gene['samples'] + assert isinstance(samples, list) + assert len(samples) > 1 + for sample in gene['samples'][0:10]: + assert type(sample['name']) is str + assert type(sample['rnaSeqExpr']) is float + assert sample['rnaSeqExpr'] >= min_rna_seq_expr + + +def test_genes_nanostring_query_with_gene_and_sample(client, nanostring_query, nanostring_entrez_id, nanostring_sample): + response = client.post( + '/api', json={ + 'query': nanostring_query, + 'variables': { + 'entrez': [nanostring_entrez_id], + 'sample': [nanostring_sample] + } + }) + json_data = json.loads(response.data) + page = json_data['data']['genes'] + genes = page['items'] + assert isinstance(genes, list) + assert len(genes) == 1 + gene = genes[0] + assert gene['entrez'] == nanostring_entrez_id + samples = gene['samples'] + assert isinstance(samples, list) + assert len(samples) == 1 + s = samples[0] + assert type(s['name']) is str + assert type(s['nanostringExpr']) is float + assert s['name'] == nanostring_sample + + +def test_pseudobulk_query_with_entrez(client, pseudobulk_query): + response = client.post( + '/api', + json={ + 'query': pseudobulk_query, + 'variables': { + 'entrez': [135] + } + }) + json_data = json.loads(response.data) + page = json_data['data']['genes'] + genes = page['items'] + assert isinstance(genes, list) + assert len(genes) == 1 + gene = genes[0] + assert gene['entrez'] == 135 + samples = gene['pseudoBulkSamples'] + assert isinstance(samples, list) + assert len(samples) >= 10 + for sample in samples[0:10]: + assert isinstance(sample['name'], str) + assert isinstance(sample['cellType'], str) + assert isinstance(sample['singleCellSeqSum'], float) + + +def test_cells_query_with_entrez(client, cells_query): + response = client.post( + '/api', json={ + 'query': cells_query, + 'variables': { + 'entrez': [135] + } + }) + json_data = json.loads(response.data) + page = json_data['data']['genes'] + genes = page['items'] + assert isinstance(genes, list) + assert len(genes) == 1 + gene = genes[0] + assert gene['entrez'] == 135 + cells = gene['cells'] + assert isinstance(cells, list) + assert len(cells) > 0 + for cell in cells[0:10]: + assert isinstance(cell['type'], str) + assert isinstance(cell['name'], str) + assert isinstance(cell['singleCellSeq'], float) diff --git a/apps/iatlas/api-gitlab/tests/queries/test_germlineGwasResults_query.py b/apps/iatlas/api-gitlab/tests/queries/test_germlineGwasResults_query.py new file mode 100644 index 0000000000..e0e73a0387 --- /dev/null +++ b/apps/iatlas/api-gitlab/tests/queries/test_germlineGwasResults_query.py @@ -0,0 +1,289 @@ +import json +import pytest +from tests import NoneType +from api.resolvers.resolver_helpers.paging_utils import from_cursor_hash, to_cursor_hash, Paging +from api.database import return_germline_gwas_result_query + + +@pytest.fixture(scope='module') +def test_ggr(): + return { + "dataset": "TCGA", + "feature": "Module3_IFN_score", + "snp": "3:133016759:C:G", + "germline_category": "Expression Signature", + "germline_module": "IFN Response" + } + + +@pytest.fixture(scope='module') +def ggr_feature(): + return 'Cell_Proportion_B_Cells_Memory_Binary_MedianLowHigh' + + +@pytest.fixture(scope='module') +def ggr_germline_module(): + return 'Unassigned' + + +@pytest.fixture(scope='module') +def ggr_germline_category(): + return 'Leukocyte Subset %' + + +@pytest.fixture(scope='module') +def ggr_snp(): + return '7:104003135:C:G' + + +@pytest.fixture(scope='module') +def ggr_max_p_value(): + # return 0.000000000000712 + return 9.9e-8 + + +@pytest.fixture(scope='module') +def ggr_min_p_value(): + return 1.0e-07 + + +@pytest.fixture(scope='module') +def common_query_builder(): + def f(query_fields): + return """query GermlineGwasResults( + $paging: PagingInput + $distinct: Boolean + $dataSet: [String!] + $feature: [String!] + $snp: [String!] + $minPValue: Float + $maxPValue: Float + ) { + germlineGwasResults( + paging: $paging + distinct: $distinct + dataSet: $dataSet + feature: $feature + snp: $snp + minPValue: $minPValue + maxPValue: $maxPValue + )""" + query_fields + "}" + return f + + +@pytest.fixture(scope='module') +def common_query(common_query_builder): + return common_query_builder("""{ + items { + dataSet { name } + feature { + name + germlineCategory + germlineModule + } + snp { name } + pValue + maf + } + paging { + type + pages + total + startCursor + endCursor + hasPreviousPage + hasNextPage + page + limit + } + error + }""") + + +# Test that forward cursor pagination gives us the expected paginInfo + + +def test_germlineGwasResults_cursor_pagination_first(client, common_query_builder): + query = common_query_builder("""{ + items { + id + } + paging { + type + pages + total + startCursor + endCursor + hasPreviousPage + hasNextPage + page + limit + } + }""") + num = 10 + response = client.post( + '/api', json={'query': query, 'variables': { + 'paging': {'first': num} + }}) + json_data = json.loads(response.data) + page = json_data['data']['germlineGwasResults'] + items = page['items'] + paging = page['paging'] + start = from_cursor_hash(paging['startCursor']) + end = from_cursor_hash(paging['endCursor']) + + assert len(items) == num + assert paging['hasNextPage'] == True + assert paging['hasPreviousPage'] == False + assert start == items[0]['id'] + assert end == items[num - 1]['id'] + + +def test_germlineGwasResults_cursor_pagination_last(client, common_query_builder): + query = common_query_builder("""{ + items { + id + } + paging { + type + pages + total + startCursor + endCursor + hasPreviousPage + hasNextPage + page + limit + } + }""") + num = 10 + response = client.post( + '/api', json={'query': query, 'variables': { + 'paging': { + 'last': num, + } + }}) + json_data = json.loads(response.data) + page = json_data['data']['germlineGwasResults'] + items = page['items'] + paging = page['paging'] + start = from_cursor_hash(paging['startCursor']) + end = from_cursor_hash(paging['endCursor']) + + assert len(items) == num + assert paging['hasNextPage'] == False + assert paging['hasPreviousPage'] == True + assert start == items[0]['id'] + assert end == items[num - 1]['id'] + + +def test_germlineGwasResults_cursor_distinct_pagination(client, common_query): + page_num = 1 + num = 10 + response = client.post( + '/api', json={'query': common_query, 'variables': { + 'paging': { + 'page': page_num, + 'first': num, + }, + 'distinct': True, + 'dataSet': ['TCGA'] + }}) + json_data = json.loads(response.data) + page = json_data['data']['germlineGwasResults'] + items = page['items'] + + assert len(items) == num + assert page_num == page['paging']['page'] + + +def test_germlineGwasResults_query_with_passed_dataset_snp_and_feature(client, common_query, test_ggr): + response = client.post('/api', json={'query': common_query, 'variables': { + 'dataSet': [test_ggr["dataset"]], + 'feature': [test_ggr["feature"]], + 'snp': [test_ggr["snp"]] + }}) + json_data = json.loads(response.data) + page = json_data['data']['germlineGwasResults'] + results = page['items'] + assert isinstance(results, list) + assert len(results) > 0 + for result in results[0:2]: + assert result['dataSet']['name'] == test_ggr["dataset"] + assert result['feature']['name'] == test_ggr["feature"] + assert result['snp']['name'] == test_ggr["snp"] + + +def test_germlineGwasResults_query_with_passed_min_p_value(client, common_query_builder, ggr_min_p_value): + query = common_query_builder( + """{ + items { pValue } + }""" + ) + response = client.post( + '/api', json={'query': query, 'variables': { + 'minPValue': ggr_min_p_value + }}) + json_data = json.loads(response.data) + page = json_data['data']['germlineGwasResults'] + results = page['items'] + assert isinstance(results, list) + assert len(results) > 0 + for result in results[0:2]: + assert result['pValue'] >= ggr_min_p_value + + +def test_germlineGwasResults_query_with_passed_max_p_value(client, common_query_builder, ggr_max_p_value): + query = common_query_builder( + """{ + items { pValue } + }""" + ) + response = client.post( + '/api', json={'query': query, 'variables': { + 'maxPValue': ggr_max_p_value + }}) + json_data = json.loads(response.data) + page = json_data['data']['germlineGwasResults'] + results = page['items'] + assert isinstance(results, list) + assert len(results) > 0 + for result in results[0:2]: + assert result['pValue'] <= ggr_max_p_value + + +def test_germlineGwasResults_query_with_no_arguments(client, common_query_builder): + query = common_query_builder("""{ + items { + pValue + feature { + name + } + } + }""") + response = client.post('/api', json={'query': query}) + json_data = json.loads(response.data) + page = json_data['data']['germlineGwasResults'] + germline_gwas_results = page['items'] + # Get the total number of hr results in the database. + ggr_count = return_germline_gwas_result_query('id').count() + + assert isinstance(germline_gwas_results, list) + assert len(germline_gwas_results) == ggr_count + for germline_gwas_result in germline_gwas_results[0:2]: + assert type(germline_gwas_result['pValue']) is float or NoneType + + +def test_germlineGwasResults_query_with_germline_fetaure(client, common_query, test_ggr): + response = client.post('/api', json={'query': common_query, 'variables': { + 'feature': [test_ggr["feature"]] + }}) + json_data = json.loads(response.data) + page = json_data['data']['germlineGwasResults'] + results = page['items'] + assert isinstance(results, list) + assert len(results) > 1 + for result in results: + assert result['feature']['name'] == test_ggr["feature"] + assert result['feature']['germlineCategory'] == test_ggr["germline_category"] + assert result['feature']['germlineModule'] == test_ggr["germline_module"] diff --git a/apps/iatlas/api-gitlab/tests/queries/test_heritabilityResults_query.py b/apps/iatlas/api-gitlab/tests/queries/test_heritabilityResults_query.py new file mode 100644 index 0000000000..105085fbe3 --- /dev/null +++ b/apps/iatlas/api-gitlab/tests/queries/test_heritabilityResults_query.py @@ -0,0 +1,248 @@ +import json +import pytest +from tests import NoneType +from api.resolvers.resolver_helpers.paging_utils import from_cursor_hash, to_cursor_hash, Paging +from api.database import return_heritability_result_query + + +@pytest.fixture(scope='module') +def hr_feature(): + return 'BCR_Richness' + + +@pytest.fixture(scope='module') +def hr_germline_module(): + return 'Unassigned' + + +@pytest.fixture(scope='module') +def hr_germline_category(): + return 'Adaptive Receptor' + + +@pytest.fixture(scope='module') +def common_query_builder(): + def f(query_fields): + return """query HeritabilityResults( + $paging: PagingInput + $distinct:Boolean + $dataSet: [String!] + $feature: [String!] + $cluster: [String!] + $minPValue: Float + $maxPValue: Float + ) { + heritabilityResults( + paging: $paging + distinct: $distinct + dataSet: $dataSet + feature: $feature + cluster: $cluster + minPValue: $minPValue + maxPValue: $maxPValue + )""" + query_fields + "}" + return f + + +@pytest.fixture(scope='module') +def common_query(common_query_builder): + return common_query_builder("""{ + items { + pValue + dataSet { name } + feature { + name + display + unit + order + germlineModule + germlineCategory + } + cluster + fdr + variance + se + } + paging { + type + pages + total + startCursor + endCursor + hasPreviousPage + hasNextPage + page + limit + } + error + }""") + + +@pytest.fixture(scope='module') +def max_p_value(): + return 0.1 + + +@pytest.fixture(scope='module') +def min_p_value(): + return 0.493599999999999983213 + + +def test_heritabilityResults_cursor_pagination_first(client, common_query_builder): + query = common_query_builder("""{ + items { + id + } + paging { + type + pages + total + startCursor + endCursor + hasPreviousPage + hasNextPage + page + limit + } + }""") + num = 10 + response = client.post( + '/api', json={'query': query, 'variables': { + 'paging': {'first': num} + }}) + json_data = json.loads(response.data) + page = json_data['data']['heritabilityResults'] + items = page['items'] + paging = page['paging'] + start = from_cursor_hash(paging['startCursor']) + end = from_cursor_hash(paging['endCursor']) + + assert len(items) == num + assert paging['hasNextPage'] == True + assert paging['hasPreviousPage'] == False + assert start == items[0]['id'] + assert end == items[num - 1]['id'] + + +def test_heritabilityResults_cursor_pagination_last(client, common_query_builder): + query = common_query_builder("""{ + items { + id + } + paging { + type + pages + total + startCursor + endCursor + hasPreviousPage + hasNextPage + page + limit + } + }""") + num = 10 + response = client.post( + '/api', json={'query': query, 'variables': { + 'paging': { + 'last': num, + } + }}) + json_data = json.loads(response.data) + page = json_data['data']['heritabilityResults'] + items = page['items'] + paging = page['paging'] + start = from_cursor_hash(paging['startCursor']) + end = from_cursor_hash(paging['endCursor']) + + assert len(items) == num + assert paging['hasNextPage'] == False + assert paging['hasPreviousPage'] == True + assert start == items[0]['id'] + assert end == items[num - 1]['id'] + + +def test_heritabilityResults_cursor_distinct_pagination(client, common_query): + page_num = 2 + num = 10 + response = client.post( + '/api', json={'query': common_query, 'variables': { + 'paging': { + 'page': page_num, + 'first': num, + }, + 'distinct': True, + 'dataSet': ['TCGA'], + 'tag': ['C1'] + }}) + json_data = json.loads(response.data) + page = json_data['data']['heritabilityResults'] + items = page['items'] + + assert len(items) == num + assert page_num == page['paging']['page'] + + +def test_heritabilityResults_query_with_passed_data_set_and_feature(client, common_query, data_set, hr_feature, hr_germline_module, hr_germline_category): + response = client.post('/api', json={'query': common_query, 'variables': { + 'dataSet': [data_set], + 'feature': [hr_feature] + }}) + json_data = json.loads(response.data) + page = json_data['data']['heritabilityResults'] + results = page['items'] + assert isinstance(results, list) + assert len(results) > 0 + for result in results[0:2]: + assert result['dataSet']['name'] == data_set + assert result['feature']['name'] == hr_feature + assert type(result['feature']['display']) is str + assert type(result['feature']['unit']) is str + assert type(result['feature']['order']) is int + assert result['feature']['germlineModule'] == hr_germline_module + assert result['feature']['germlineCategory'] == hr_germline_category + + +def test_heritabilityResults_query_with_passed_min_p_value(client, common_query, min_p_value): + response = client.post( + '/api', json={'query': common_query, 'variables': { + 'minPValue': min_p_value + }}) + json_data = json.loads(response.data) + page = json_data['data']['heritabilityResults'] + results = page['items'] + assert isinstance(results, list) + assert len(results) > 0 + for result in results[0:2]: + assert result['pValue'] >= min_p_value + + +def test_heritabilityResults_query_with_passed_max_p_value(client, common_query, max_p_value): + response = client.post( + '/api', json={'query': common_query, 'variables': { + 'maxPValue': max_p_value + }}) + json_data = json.loads(response.data) + page = json_data['data']['heritabilityResults'] + results = page['items'] + assert isinstance(results, list) + assert len(results) > 0 + for result in results[0:2]: + assert result['pValue'] <= max_p_value + + +def test_heritabilityResults_query_with_no_arguments(client, common_query_builder): + query = common_query_builder("""{ + items { id } + }""") + response = client.post('/api', json={'query': query}) + json_data = json.loads(response.data) + page = json_data['data']['heritabilityResults'] + heritability_results = page['items'] + # Get the total number of hr results in the database. + hr_count = return_heritability_result_query('id').count() + + assert isinstance(heritability_results, list) + assert len(heritability_results) == hr_count + for heritability_result in heritability_results[0:2]: + assert type(heritability_result['id']) is str diff --git a/apps/iatlas/api-gitlab/tests/queries/test_mutation_types_query.py b/apps/iatlas/api-gitlab/tests/queries/test_mutation_types_query.py new file mode 100644 index 0000000000..f642aaf456 --- /dev/null +++ b/apps/iatlas/api-gitlab/tests/queries/test_mutation_types_query.py @@ -0,0 +1,21 @@ +import json +import pytest +from tests import NoneType + + +def test_mutation_types_query(client): + query = """query MutationTypes { + mutationTypes { + display + name + } + }""" + response = client.post('/api', json={'query': query}) + json_data = json.loads(response.data) + mutation_types = json_data['data']['mutationTypes'] + + assert isinstance(mutation_types, list) + assert len(mutation_types) > 0 + for mutation_type in mutation_types: + assert type(mutation_type['name']) is str + assert type(mutation_type['display']) is str or NoneType diff --git a/apps/iatlas/api-gitlab/tests/queries/test_mutations_query.py b/apps/iatlas/api-gitlab/tests/queries/test_mutations_query.py new file mode 100644 index 0000000000..afa63f376e --- /dev/null +++ b/apps/iatlas/api-gitlab/tests/queries/test_mutations_query.py @@ -0,0 +1,367 @@ +from enum import unique +import json +import pytest +from sqlalchemy import and_ +from tests import NoneType +from api.resolvers.resolver_helpers.paging_utils import from_cursor_hash, to_cursor_hash, Paging + + +@pytest.fixture(scope='module') +def gene_entrez(): + return 92 + + +@pytest.fixture(scope='module') +def mutation_id(): + return 291 + + +@pytest.fixture(scope='module') +def mutation_code(): + return 'G12' + + +@pytest.fixture(scope='module') +def mutation_status(): + return 'Mut' + + +@pytest.fixture(scope='module') +def sample_name(): + return 'TCGA-38-7271' + + +@pytest.fixture(scope='module') +def dr_mutation(): + return 'ABL1:(NS)' + +@pytest.fixture(scope='module') +def mutation_type_name(): + return 'driver_mutation' + + +@pytest.fixture(scope='module') +def common_query_builder(): + def f(query_fields): + return """query Mutations( + $paging: PagingInput + $distinct: Boolean + $cohort: [String!] + $entrez: [Int!] + $mutation: [String!] + $mutationCode: [String!] + $mutationType: [String!] + $sample: [String!] + $status: [StatusEnum!] + ) { + mutations( + paging: $paging + distinct: $distinct + cohort: $cohort + entrez: $entrez + mutation: $mutation + mutationCode: $mutationCode + mutationType: $mutationType + sample: $sample + status: $status + )""" + query_fields + "}" + return f + + +@pytest.fixture(scope='module') +def common_query(common_query_builder): + return common_query_builder(""" + { + items { + id + name + gene { entrez } + mutationCode + mutationType { name } + } + paging { + type + pages + total + startCursor + endCursor + hasPreviousPage + hasNextPage + page + limit + } + error + }""" + ) + + +@pytest.fixture(scope='module') +def samples_query(common_query_builder): + return common_query_builder(""" + { + items { + id + name + gene { entrez } + mutationCode + mutationType { name } + samples { + name + status + } + } + paging { + type + pages + total + startCursor + endCursor + hasPreviousPage + hasNextPage + page + limit + } + error + }""" + ) + + +def test_mutations_cursor_pagination_first_without_samples(client, common_query_builder): + query = common_query_builder("""{ + items { + id + } + paging { + type + pages + total + startCursor + endCursor + hasPreviousPage + hasNextPage + page + limit + } + }""") + requested_n = 15 + response = client.post( + '/api', json={'query': query, 'variables': { + 'paging': {'first': requested_n} + }}) + json_data = json.loads(response.data) + page = json_data['data']['mutations'] + items = page['items'] + paging = page['paging'] + start = from_cursor_hash(paging['startCursor']) + end = from_cursor_hash(paging['endCursor']) + + assert len(items) == requested_n + assert paging['hasNextPage'] == True + assert paging['hasPreviousPage'] == False + + +def test_mutationscursor_pagination_first_with_samples(client, common_query_builder): + query = common_query_builder("""{ + items { + id + samples { name } + } + paging { + type + pages + total + startCursor + endCursor + hasPreviousPage + hasNextPage + page + limit + } + }""") + requested_n = 15 + max_n = 10 + response = client.post( + '/api', json={'query': query, 'variables': { + 'paging': {'first': requested_n} + }}) + json_data = json.loads(response.data) + page = json_data['data']['mutations'] + items = page['items'] + paging = page['paging'] + start = from_cursor_hash(paging['startCursor']) + end = from_cursor_hash(paging['endCursor']) + + assert len(items) == max_n + assert paging['hasNextPage'] == True + assert paging['hasPreviousPage'] == False + + +def test_cursor_pagination_last(client, common_query_builder): + query = common_query_builder("""{ + items { + id + } + paging { + type + pages + total + startCursor + endCursor + hasPreviousPage + hasNextPage + page + limit + } + }""") + num = 10 + response = client.post( + '/api', json={'query': query, 'variables': { + 'paging': { + 'last': num, + 'before': to_cursor_hash(1000) + } + }}) + json_data = json.loads(response.data) + page = json_data['data']['mutations'] + items = page['items'] + paging = page['paging'] + start = from_cursor_hash(paging['startCursor']) + end = from_cursor_hash(paging['endCursor']) + + assert len(items) == num + assert paging['hasNextPage'] == False + assert paging['hasPreviousPage'] == True + + +def test_mutations_query_with_mutation_name(client, common_query, dr_mutation): + response = client.post( + '/api', json={'query': common_query, 'variables': {'mutation': [dr_mutation]}}) + json_data = json.loads(response.data) + mutations = json_data['data']['mutations'] + page = mutations['items'] + + assert isinstance(page, list) + assert len(page) == 1 + for mutation in page: + assert mutation['name'] == dr_mutation + + +def test_mutations_query_with_entrez(client, samples_query, gene_entrez): + response = client.post( + '/api', json={'query': samples_query, 'variables': {'entrez': [gene_entrez]}}) + json_data = json.loads(response.data) + mutations = json_data['data']['mutations'] + page = mutations['items'] + + assert isinstance(page, list) + assert len(page) > 0 + for mutation in page[0:2]: + samples = mutation['samples'] + assert mutation['gene']['entrez'] == gene_entrez + assert type(mutation['mutationCode']) is str + assert type(mutation['mutationType']['name']) is str + assert isinstance(samples, list) + assert len(samples) > 0 + for sample in samples: + assert type(sample['name']) is str + + +def test_mutations_query_with_mutationCode(client, common_query, mutation_code): + response = client.post( + '/api', json={'query': common_query, 'variables': {'mutationCode': [mutation_code]}}) + json_data = json.loads(response.data) + mutations = json_data['data']['mutations'] + page = mutations['items'] + + assert isinstance(page, list) + assert len(mutations) > 0 + for mutation in page[0:2]: + assert mutation['mutationCode'] == mutation_code + + +def test_mutations_query_with_mutationType(client, common_query, mutation_type_name): + response = client.post( + '/api', json={'query': common_query, 'variables': {'mutationType': [mutation_type_name]}}) + json_data = json.loads(response.data) + mutations = json_data['data']['mutations'] + page = mutations['items'] + + assert isinstance(page, list) + assert len(page) > 0 + for mutation in page[0:2]: + assert mutation['mutationType']['name'] == mutation_type_name + + +def test_mutations_query_with_sample(client, samples_query, sample_name): + response = client.post( + '/api', json={'query': samples_query, 'variables': {'sample': [sample_name]}}) + json_data = json.loads(response.data) + mutations = json_data['data']['mutations'] + page = mutations['items'] + + assert isinstance(page, list) + assert len(page) > 0 + for mutation in page[0:2]: + samples = mutation['samples'] + assert isinstance(samples, list) + assert len(samples) > 0 + for current_sample in samples: + assert current_sample['name'] == sample_name + + +def test_mutations_query_with_status(client, samples_query, mutation_status): + num = 5 + response = client.post( + '/api', json={'query': samples_query, 'variables': {'paging': {'first': num}, 'status': [mutation_status]}}) + json_data = json.loads(response.data) + mutations = json_data['data']['mutations'] + page = mutations['items'] + + assert isinstance(page, list) + assert len(page) == num + for mutation in page[0:2]: + samples = mutation['samples'] + assert isinstance(samples, list) + assert len(samples) > 0 + for current_sample in samples: + assert current_sample['status'] == mutation_status + + +def test_mutations_query_with_no_variables(client, common_query): + response = client.post( + '/api', json={'query': common_query}) + json_data = json.loads(response.data) + mutations = json_data['data']['mutations'] + page = mutations['items'] + + assert isinstance(page, list) + assert len(page) > 0 + for mutation in page[0:2]: + assert type(mutation['id']) is not None + + +def test_mutations_query_with_cohort(client, samples_query, tcga_tag_cohort_name, tcga_tag_cohort_samples): + num = 1 + response = client.post( + '/api', json={ + 'query': samples_query, + 'variables': { + 'paging': {'first': num}, + 'cohort': [tcga_tag_cohort_name] + } + } + ) + json_data = json.loads(response.data) + mutations = json_data['data']['mutations'] + page = mutations['items'] + + assert isinstance(page, list) + assert len(page) == num + for mutation in page[0:2]: + samples = mutation['samples'] + assert isinstance(samples, list) + assert len(samples) > 0 + for sample in samples: + assert type(sample['name']) is str + assert type(sample['status']) is str + assert sample['name'] in tcga_tag_cohort_samples diff --git a/apps/iatlas/api-gitlab/tests/queries/test_neoantigens_query.py b/apps/iatlas/api-gitlab/tests/queries/test_neoantigens_query.py new file mode 100644 index 0000000000..b12b0ff202 --- /dev/null +++ b/apps/iatlas/api-gitlab/tests/queries/test_neoantigens_query.py @@ -0,0 +1,230 @@ +import json +import pytest +from tests import NoneType +from api.resolvers.resolver_helpers.paging_utils import from_cursor_hash, to_cursor_hash, Paging +from api.db_models import Neoantigen, Gene, Patient + + +@pytest.fixture(scope='module') +def test_neoantigen(test_db): + query = test_db.session.query( + Neoantigen.id, + Neoantigen.patient_id, + Neoantigen.neoantigen_gene_id, + Neoantigen.pmhc, + Neoantigen.freq_pmhc, + Neoantigen.tpm + ) + query = query.filter(Neoantigen.neoantigen_gene_id.isnot(None)).limit(1) + return query.one_or_none() + + +@pytest.fixture(scope='module') +def test_gene(test_db, test_neoantigen): + query = test_db.session.query( + Gene.id, + Gene.entrez_id, + Gene.hgnc_id + ) + query = query.filter_by(id=test_neoantigen.neoantigen_gene_id) + return query.one_or_none() + + +@pytest.fixture(scope='module') +def test_patient(test_db, test_neoantigen): + query = test_db.session.query( + Patient.id, + Patient.name + ) + query = query.filter_by(id=test_neoantigen.patient_id) + return query.one_or_none() + + +@pytest.fixture(scope='module') +def common_query_builder(): + def f(query_fields): + return """query Neoantigens( + $paging: PagingInput + $distinct:Boolean + $entrez: [Int!] + $patient: [String!] + $pmhc: [String!] + ) { + neoantigens( + paging: $paging + distinct: $distinct + entrez: $entrez + patient: $patient + pmhc: $pmhc + )""" + query_fields + "}" + return f + + +@pytest.fixture(scope='module') +def common_query(common_query_builder): + return common_query_builder("""{ + items { + id + tpm + pmhc + freqPmhc + patient { barcode } + gene { + entrez + hgnc + } + } + paging { + type + pages + total + startCursor + endCursor + hasPreviousPage + hasNextPage + page + limit + } + error + }""") + + +def test_cursor_pagination_first(client, common_query_builder): + query = common_query_builder("""{ + items { + id + } + paging { + type + pages + total + startCursor + endCursor + hasPreviousPage + hasNextPage + page + limit + } + }""") + num = 10 + response = client.post( + '/api', json={'query': query, 'variables': { + 'paging': {'first': num} + }}) + json_data = json.loads(response.data) + page = json_data['data']['neoantigens'] + items = page['items'] + paging = page['paging'] + start = from_cursor_hash(paging['startCursor']) + end = from_cursor_hash(paging['endCursor']) + + assert len(items) == num + assert paging['hasNextPage'] == True + assert paging['hasPreviousPage'] == False + assert start == items[0]['id'] + assert end == items[num - 1]['id'] + + +def test_cursor_pagination_last(client, common_query_builder): + query = common_query_builder("""{ + items { + id + } + paging { + type + pages + total + startCursor + endCursor + hasPreviousPage + hasNextPage + page + limit + } + }""") + num = 10 + response = client.post( + '/api', json={'query': query, 'variables': { + 'paging': { + 'last': num, + } + }}) + json_data = json.loads(response.data) + page = json_data['data']['neoantigens'] + items = page['items'] + paging = page['paging'] + start = from_cursor_hash(paging['startCursor']) + end = from_cursor_hash(paging['endCursor']) + + assert len(items) == num + assert paging['hasNextPage'] == False + assert paging['hasPreviousPage'] == True + assert start == items[0]['id'] + assert end == items[num - 1]['id'] + + +def test_cursor_distinct_pagination(client, common_query): + page_num = 2 + num = 10 + response = client.post( + '/api', json={'query': common_query, 'variables': { + 'paging': { + 'page': page_num, + 'first': num, + }, + 'distinct': True, + }}) + json_data = json.loads(response.data) + page = json_data['data']['neoantigens'] + items = page['items'] + + assert len(items) == num + assert page_num == page['paging']['page'] + + +def test_query(client, common_query): + response = client.post( + '/api', json={ + 'query': common_query, + 'variables': {'paging': {'first': 3, }} + } + ) + json_data = json.loads(response.data) + page = json_data['data']['neoantigens'] + results = page['items'] + assert isinstance(results, list) + assert len(results) > 0 + for result in results[0:3]: + assert type(result['id']) is str + assert type(result['tpm']) is float or NoneType + assert type(result['pmhc']) is str + assert type(result['freqPmhc']) is int + assert type(result['gene']['entrez']) is int or NoneType + assert type(result['gene']['hgnc']) is int or NoneType + assert type(result['patient']['barcode']) is str + + +def test_query_specific_neoantigen(client, common_query, test_neoantigen, test_gene, test_patient): + response = client.post( + '/api', json={ + 'query': common_query, + 'variables': { + 'pmhc': [test_neoantigen.pmhc], + 'entrez': [test_gene.entrez_id], + 'patient': [test_patient.name] + } + } + ) + json_data = json.loads(response.data) + page = json_data['data']['neoantigens'] + results = page['items'] + assert isinstance(results, list) + assert len(results) > 0 + for result in results[0:3]: + assert type(result['id']) is str + assert result['tpm'] == test_neoantigen.tpm + assert result['pmhc'] == test_neoantigen.pmhc + assert result['freqPmhc'] == test_neoantigen.freq_pmhc + assert result['gene']['entrez'] == test_gene.entrez_id + assert result['gene']['hgnc'] == test_gene.hgnc_id + assert result['patient']['barcode'] == test_patient.name diff --git a/apps/iatlas/api-gitlab/tests/queries/test_nodes_query.py b/apps/iatlas/api-gitlab/tests/queries/test_nodes_query.py new file mode 100644 index 0000000000..1a876d375f --- /dev/null +++ b/apps/iatlas/api-gitlab/tests/queries/test_nodes_query.py @@ -0,0 +1,725 @@ +import json +import pytest +from api.database import return_node_query +from tests import NoneType +from api.resolvers.resolver_helpers.paging_utils import Paging + + +@pytest.fixture(scope='module') +def node_feature(): + return 'B_cells_Aggregate2' + + +@pytest.fixture(scope='module') +def node_feature_class(): + return 'Immune Cell Proportion - Common Lymphoid and Myeloid Cell Derivative Class' + + +@pytest.fixture(scope='module') +def node_gene_type(): + return 'Chemokine12_score' + + +@pytest.fixture(scope='module') +def max_score(): + return 0.0234375 + + +@pytest.fixture(scope='module') +def min_score(): + return 0.9980582524271844891 + + +@pytest.fixture(scope='module') +def network(): + return 'Extracellular Network' + +@pytest.fixture(scope='module') +def node_entrez_id(): + return 5797 + + +@pytest.fixture(scope='module') +def common_query_builder(): + def f(query_fields): + return """query Nodes( + $paging: PagingInput + $distinct: Boolean + $dataSet: [String!] + $entrez: [Int!] + $feature: [String!] + $featureClass: [String!] + $geneType: [String!] + $maxScore: Float + $minScore: Float + $network: [String!] + $related: [String!] + $tag1: [String!] + $tag2: [String!] + $nTags: Int + ) { + nodes( + paging: $paging + distinct: $distinct + dataSet: $dataSet + entrez: $entrez + feature: $feature + featureClass: $featureClass + geneType: $geneType + maxScore: $maxScore + minScore: $minScore + network: $network + related: $related + tag1: $tag1 + tag2: $tag2 + nTags: $nTags + )""" + query_fields + "}" + return f + + +@pytest.fixture(scope='module') +def common_query(common_query_builder): + return common_query_builder("""{ + items { name } + paging { + page + pages + total + returned + } + }""") + + +@pytest.fixture(scope='module') +def genes_query(common_query_builder): + return common_query_builder( + """{ + items{ + label + name + score + x + y + tags { + characteristics + color + name + longDisplay + order + shortDisplay + type + } + gene { + entrez + hgnc + friendlyName + } + } + paging { + type + pages + total + page + limit + hasNextPage + hasPreviousPage + startCursor + endCursor + } + error + }""") + + +def test_nodes_query_with_passed_data_set(client, common_query, data_set): + response = client.post( + '/api', json={'query': common_query, 'variables': {'dataSet': [data_set]}}) + json_data = json.loads(response.data) + page = json_data['data']['nodes'] + results = page['items'] + paging = page['paging'] + + assert type(paging['page']) is NoneType + assert type(paging['pages']) is int + assert type(paging['total']) is int + assert type(paging['returned']) is int + assert isinstance(results, list) + assert len(results) > 0 + for result in results[0:2]: + assert type(result['name']) is str + + +def test_nodes_query_with_passed_data_set_page2(client, common_query_builder, data_set): + + query = common_query_builder("""{ + items { name } + paging { + endCursor + startCursor + } + }""") + response = client.post( + '/api', json={'query': query, 'variables': {'dataSet': [data_set], "paging": {"limit": 10}}}) + json_data = json.loads(response.data) + page = json_data['data']['nodes'] + results = page['items'] + paging = page['paging'] + assert type(paging['endCursor']) is str + assert type(paging['startCursor']) is str + assert isinstance(results, list) + assert len(results) == 10 + for result in results[0:2]: + assert type(result['name']) is str + + response = client.post( + '/api', json={'query': query, 'variables': {'dataSet': [data_set], "paging": {"limit": 10, "after": paging['endCursor']}}}) + json_data = json.loads(response.data) + page = json_data['data']['nodes'] + results = page['items'] + paging = page['paging'] + assert type(paging['startCursor']) is str + assert type(paging['endCursor']) is str + assert isinstance(results, list) + assert len(results) == 10 + for result in results[0:2]: + assert type(result['name']) is str + + +def test_nodes_query_with_passed_data_set_offset(client, common_query_builder, data_set): + query = common_query_builder("""{ + items { name } + paging { + page + pages + total + } + }""") + response = client.post('/api', json={'query': query, + 'variables': {'paging': {'type': Paging.OFFSET, 'page': 2}, 'dataSet': [data_set], }}) + json_data = json.loads(response.data) + page = json_data['data']['nodes'] + results = page['items'] + paging = page['paging'] + + assert paging['page'] == 2 + assert type(paging['pages']) is int + assert type(paging['total']) is int + assert isinstance(results, list) + assert len(results) > 0 + for result in results[0:2]: + assert type(result['name']) is str + + +def test_nodes_query_with_passed_related(client, common_query_builder, related): + query = common_query_builder("""{ + items { + name + gene { entrez } + } + }""") + response = client.post('/api', json={'query': query, + 'variables': {'related': [related]}}) + json_data = json.loads(response.data) + page = json_data['data']['nodes'] + results = page['items'] + + assert isinstance(results, list) + assert len(results) > 0 + for result in results[0:2]: + gene = result['gene'] + assert type(result['name']) is str + if gene: + assert type(gene['entrez']) is int + + +def test_nodes_query_with_passed_entrez(client, common_query_builder, node_entrez_id): + query = common_query_builder("""{ + items { + name + gene { entrez } + } + }""") + response = client.post('/api', json={'query': query, + 'variables': {'entrez': [node_entrez_id]}}) + json_data = json.loads(response.data) + page = json_data['data']['nodes'] + results = page['items'] + + assert isinstance(results, list) + assert len(results) > 0 + for result in results[0:2]: + gene = result['gene'] + assert type(result['name']) is str + assert gene['entrez'] == node_entrez_id + + +def test_nodes_query_with_passed_feature(client, common_query_builder, node_feature): + query = common_query_builder("""{ + items { + name + feature { name } + } + }""") + response = client.post('/api', json={'query': query, + 'variables': {'feature': [node_feature]}}) + json_data = json.loads(response.data) + page = json_data['data']['nodes'] + results = page['items'] + + assert isinstance(results, list) + assert len(results) > 0 + for result in results[0:2]: + feature = result['feature'] + assert type(result['name']) is str + assert feature['name'] == node_feature + + +def test_nodes_query_with_passed_featureClass(client, common_query_builder, node_feature_class): + query = common_query_builder("""{ + items { + name + feature { name } + } + }""") + response = client.post('/api', json={'query': query, + 'variables': {'featureClass': ['does_not_exist']}}) + json_data = json.loads(response.data) + page = json_data['data']['nodes'] + results = page['items'] + + assert isinstance(results, list) + assert len(results) == 0 + + response = client.post('/api', json={'query': query, + 'variables': {'featureClass': [node_feature_class]}}) + json_data = json.loads(response.data) + page = json_data['data']['nodes'] + results = page['items'] + + assert isinstance(results, list) + assert len(results) > 0 + for result in results[0:2]: + feature = result['feature'] + assert type(result['name']) is str + assert type(feature['name']) is str + + +def test_nodes_query_with_passed_geneType(client, common_query_builder, node_gene_type): + query = common_query_builder("""{ + items { + name + gene { entrez } + } + }""") + response = client.post('/api', json={'query': query, + 'variables': {'geneType': ['does_not_exist']}}) + json_data = json.loads(response.data) + page = json_data['data']['nodes'] + results = page['items'] + + assert isinstance(results, list) + assert len(results) == 0 + + response = client.post('/api', json={'query': query, + 'variables': {'geneType': [node_gene_type]}}) + json_data = json.loads(response.data) + page = json_data['data']['nodes'] + results = page['items'] + + assert isinstance(results, list) + assert len(results) > 0 + for result in results[0:2]: + gene = result['gene'] + assert type(result['name']) is str + assert type(gene['entrez']) is int + + +def test_nodes_query_with_passed_network(client, common_query_builder, network): + query = common_query_builder("""{ + items { + label + name + score + x + y + network + feature { name } + tag1 { name } + } + }""") + num = 200 + response = client.post('/api', json={'query': query, + 'variables': { + 'network': [network], + 'paging': {'first': num} + } + }) + json_data = json.loads(response.data) + page = json_data['data']['nodes'] + results = page['items'] + + assert isinstance(results, list) + assert len(results) == num + for result in results[0:2]: + feature = result['feature'] + assert result['network'] == network + assert type(result['label']) is str or NoneType + assert type(result['name']) is str + assert type(result['score']) is float or NoneType + assert type(result['x']) is float or NoneType + assert type(result['y']) is float or NoneType + assert type(result['tag1']['name']) is str + if feature: + assert type(feature['name']) is str + + +def test_nodes_query_with_passed_network_and_tag(client, common_query_builder, network, tag): + query = common_query_builder("""{ + items { + label + name + score + x + y + network + gene { entrez } + tag1 { name } + } + }""") + num = 1000 + response = client.post('/api', json={'query': query, + 'variables': { + 'network': [network], + 'tag1': [tag], + 'paging': {'first': num} + } + }) + json_data = json.loads(response.data) + page = json_data['data']['nodes'] + results = page['items'] + + assert isinstance(results, list) + assert len(results) > 0 + for result in results[0:2]: + gene = result['gene'] + assert result['network'] == network + assert result['tag1']['name'] == tag + assert type(result['label']) is str or NoneType + assert type(result['name']) is str + assert type(result['score']) is float or NoneType + assert type(result['x']) is float or NoneType + assert type(result['y']) is float or NoneType + if gene: + assert type(gene['entrez']) is int + + +def test_nodes_query_with_passed_tag(client, common_query_builder, tag): + query = common_query_builder("""{ + items { + label + name + tag1 { + name + characteristics + color + longDisplay + shortDisplay + } + } + }""") + num = 100 + response = client.post('/api', json={ + 'query': query, + 'variables': {'tag1': [tag], 'paging': {'first': num}} + }) + json_data = json.loads(response.data) + page = json_data['data']['nodes'] + results = page['items'] + + assert isinstance(results, list) + assert len(results) == num + for result in results[0:2]: + tag1 = result['tag1'] + assert tag1['name'] == tag + + +def test_nodes_query_with_passed_tag_and_entrez(client, common_query_builder, node_entrez_id, tag): + query = common_query_builder("""{ + items { + name + gene { entrez } + tag1 { name } + } + }""") + response = client.post( + '/api', + json={ + 'query': query, + 'variables': {'entrez': [node_entrez_id], 'tag1': [tag]} + } + ) + json_data = json.loads(response.data) + page = json_data['data']['nodes'] + results = page['items'] + + assert isinstance(results, list) + assert len(results) > 0 + for result in results[0:2]: + gene = result['gene'] + assert gene['entrez'] == node_entrez_id + tag1 = result['tag1'] + assert tag1['name'] == tag + + +def test_nodes_query_with_passed_tag_and_feature(client, common_query_builder, node_feature, tag): + query = common_query_builder(""" { + items { + name + feature { name } + tag1 { name } + } + } """) + response = client.post( + '/api', + json={ + 'query': query, + 'variables': { + 'feature': [node_feature], + 'tag1': [tag] + } + } + + ) + json_data = json.loads(response.data) + page = json_data['data']['nodes'] + results = page['items'] + + assert isinstance(results, list) + assert len(results) > 0 + for result in results[0:2]: + feature = result['feature'] + assert feature['name'] == node_feature + tag1 = result['tag1'] + assert tag1['name'] == tag + + +def test_nodes_query_with_passed_tag1_and_tag2(client, common_query_builder, node_feature, tag): + query = common_query_builder(""" { + items { + name + tag1 { name } + tag2 { name } + } + } """) + + response = client.post( + '/api', + json={ + 'query': query, + 'variables': { + 'tag1': ['C3'], + 'tag2': ['ACC'] + } + } + ) + + json_data = json.loads(response.data) + page = json_data['data']['nodes'] + results = page['items'] + + assert isinstance(results, list) + assert len(results) > 0 + for result in results[0:2]: + assert result['tag1']['name'] == 'C3' + assert result['tag2']['name'] == 'ACC' + + +def test_nodes_query_with_passed_maxScore(client, common_query_builder, max_score): + query = common_query_builder("""{ + items { + label + name + score + } + }""") + response = client.post('/api', json={'query': query, + 'variables': {'maxScore': max_score}}) + json_data = json.loads(response.data) + page = json_data['data']['nodes'] + results = page['items'] + + assert isinstance(results, list) + assert len(results) > 0 + for result in results[0:2]: + assert type(result['label']) is str or NoneType + assert type(result['name']) is str + assert result['score'] <= max_score + + +def test_nodes_query_with_passed_minScore(client, common_query_builder, min_score): + query = common_query_builder("""{ + items { + label + name + score + } + }""") + response = client.post('/api', json={'query': query, + 'variables': {'minScore': min_score}}) + json_data = json.loads(response.data) + page = json_data['data']['nodes'] + results = page['items'] + + assert isinstance(results, list) + assert len(results) > 0 + for result in results[0:2]: + assert type(result['label']) is str or NoneType + assert type(result['name']) is str + assert result['score'] >= min_score + + +def test_nodes_query_with_passed_maxScore_and_minScore(client, common_query_builder, max_score): + min_score = 0.015625 + query = common_query_builder("""{ + items { + label + name + score + } + }""") + response = client.post('/api', json={'query': query, + 'variables': {'maxScore': max_score, 'minScore': min_score}}) + json_data = json.loads(response.data) + page = json_data['data']['nodes'] + results = page['items'] + + assert isinstance(results, list) + assert len(results) > 0 + for result in results[0:2]: + assert type(result['label']) is str or NoneType + assert type(result['name']) is str + assert result['score'] <= max_score + assert result['score'] >= min_score + + +def test_nodes_query_with_no_arguments(client, common_query_builder): + query = common_query_builder("""{ + items { + name + dataSet { name } + } + paging { + total + } + }""") + response = client.post('/api', json={'query': query}) + json_data = json.loads(response.data) + page = json_data['data']['nodes'] + results = page['items'] + paging = page['paging'] + + # Get the total number of samples_to_mutations in the database. + node_count = return_node_query('id').count() + + assert paging['total'] == node_count + assert isinstance(results, list) + assert len(results) > 0 + for result in results[0:2]: + current_data_set = result['dataSet'] + assert type(result['name']) is str + assert type(current_data_set['name']) is str + + +def test_nodes_query_with_ntags(client, common_query_builder): + query = common_query_builder(""" { + items { + name + tag1 { name } + tag2 { name } + } + } """) + + response = client.post( + '/api', + json={ + 'query': query, + 'variables': { + 'dataSet': ['PCAWG'], + 'entrez': [4909], + 'tag1': ['C3'], + 'nTags': 1 + } + } + ) + + json_data = json.loads(response.data) + page = json_data['data']['nodes'] + results = page['items'] + + assert isinstance(results, list) + assert len(results) == 1 + result = results[0] + assert result['name'] == 'PCAWG_extracellular_network_C3_4909' + assert result['tag1']['name'] == 'C3' + assert result['tag2'] is None + + response2 = client.post( + '/api', + json={ + 'query': query, + 'variables': { + 'tag1': ['C3'], + 'tag2': ['BLCA'], + 'entrez': [651], + } + } + ) + + json_data2 = json.loads(response2.data) + page2 = json_data2['data']['nodes'] + results2 = page2['items'] + + assert isinstance(results2, list) + assert len(results2) == 1 + result2 = results2[0] + assert result2['name'] == 'TCGA_extracellular_network_C3:BLCA_651' + assert result2['tag1']['name'] == 'C3' + assert result2['tag2']['name'] == 'BLCA' + +def test_tag1_no_ntags(client, common_query_builder): + query = common_query_builder("""{ + items { + name + network + dataSet { + name + } + gene{ + entrez + } + tag1 { + name + } + tag2 { + name + } + } + }""") + + response = client.post('/api', json={ + 'query': query, + 'variables': { + 'tag1': ['C1'], + 'dataSet': ['TCGA'], + 'entrez': [2], + 'network': "Extracellular Network" + } + }) + json_data = json.loads(response.data) + page = json_data['data']['nodes'] + results = page['items'] + + for result in results: + assert result['tag1']['name'] == "C1" + assert result['dataSet']['name'] == "TCGA" + assert result['gene']['entrez'] == 2 + assert result['network'] == "Extracellular Network" diff --git a/apps/iatlas/api-gitlab/tests/queries/test_patients_query.py b/apps/iatlas/api-gitlab/tests/queries/test_patients_query.py new file mode 100644 index 0000000000..1f9e0227e8 --- /dev/null +++ b/apps/iatlas/api-gitlab/tests/queries/test_patients_query.py @@ -0,0 +1,325 @@ +import json +import pytest +from tests import NoneType +from api.enums import ethnicity_enum, gender_enum, race_enum + + +@pytest.fixture(scope='module') +def patient_name(): + return 'TCGA-WN-AB4C' + + +@pytest.fixture(scope='module') +def common_query_builder(): + def f(query_fields): + return """query Patients( + $barcode: [String!] + $dataSet: [String!] + $ethnicity: [EthnicityEnum!] + $gender: [GenderEnum!] + $maxAgeAtDiagnosis: Int + $maxHeight: Float + $maxWeight: Float + $minAgeAtDiagnosis: Int + $minHeight: Float + $minWeight: Float + $race: [RaceEnum!] + $sample: [String!] + $slide: [String!] + $paging: PagingInput + $distinct: Boolean + ) { + patients( + barcode: $barcode + dataSet: $dataSet + ethnicity: $ethnicity + gender: $gender + maxAgeAtDiagnosis: $maxAgeAtDiagnosis + maxHeight: $maxHeight + maxWeight: $maxWeight + minAgeAtDiagnosis: $minAgeAtDiagnosis + minHeight: $minHeight + minWeight: $minWeight + race: $race + sample: $sample + slide: $slide + paging: $paging + distinct: $distinct + )""" + query_fields + "}" + return f + + +@pytest.fixture(scope='module') +def common_query(common_query_builder): + return common_query_builder( + """ + { + items { + id + ageAtDiagnosis + barcode + ethnicity + gender + height + race + weight + } + paging { + type + pages + total + startCursor + endCursor + hasPreviousPage + hasNextPage + page + limit + } + error + } + """ + ) + + +@pytest.fixture(scope='module') +def full_query(common_query_builder): + return common_query_builder( + """ + { + items { + id + ageAtDiagnosis + barcode + ethnicity + gender + height + race + weight + slides { name } + samples + } + paging { + type + pages + total + startCursor + endCursor + hasPreviousPage + hasNextPage + page + limit + } + error + } + """ + ) + + +def test_patients_query_with_passed_barcode(client, full_query, patient_name): + response = client.post( + '/api', json={'query': full_query, 'variables': {'barcode': [patient_name]}}) + json_data = json.loads(response.data) + page = json_data['data']['patients'] + results = page['items'] + + assert isinstance(results, list) + assert len(results) == 1 + for result in results: + slides = result['slides'] + samples = result['samples'] + + assert type(result['ageAtDiagnosis']) is int or NoneType + assert result['barcode'] == patient_name + assert type(result['ethnicity']) in ethnicity_enum.enums or NoneType + assert type(result['gender']) in gender_enum.enums or NoneType + assert type(result['height']) is int or NoneType + assert type(result['race']) in race_enum.enums or NoneType + assert type(result['weight']) is int or NoneType + assert isinstance(slides, list) + for slide in slides: + assert type(slide['name']) is str + assert isinstance(samples, list) + for sample in samples: + assert type(sample) is str + + +def test_patients_query_with_passed_data_set(client, common_query, data_set): + response = client.post( + '/api', json={'query': common_query, 'variables': {'dataSet': [data_set]}}) + json_data = json.loads(response.data) + results = json_data['data']['patients']['items'] + + assert isinstance(results, list) + assert len(results) > 0 + for result in results[0:2]: + assert type(result['barcode']) is str + + +def test_patients_query_with_passed_slide_and_maxAgeAtDiagnosis(client, full_query, slide, max_age_at_diagnosis): + + response = client.post( + '/api', json={'query': full_query, 'variables': {'maxAgeAtDiagnosis': max_age_at_diagnosis, 'slide': [slide]}}) + json_data = json.loads(response.data) + results = json_data['data']['patients']['items'] + + assert isinstance(results, list) + assert len(results) > 0 + for result in results: + slides = result['slides'] + assert isinstance(slides, list) + assert len(slides) > 0 + for current_slide in slides: + assert current_slide['name'] == slide + samples = result['samples'] + assert isinstance(samples, list) + assert len(samples) > 0 + for current_sample in samples: + assert type(current_sample) is str + assert result['ageAtDiagnosis'] <= max_age_at_diagnosis + + +def test_patients_query_with_passed_slide_and_minAgeAtDiagnosis(client, full_query, slide, min_age_at_diagnosis): + response = client.post( + '/api', json={'query': full_query, 'variables': {'minAgeAtDiagnosis': min_age_at_diagnosis, 'slide': [slide]}}) + json_data = json.loads(response.data) + results = json_data['data']['patients']['items'] + + assert isinstance(results, list) + assert len(results) > 0 + for result in results: + slides = result['slides'] + assert isinstance(slides, list) + assert len(slides) > 0 + for current_slide in slides: + assert current_slide['name'] == slide + samples = result['samples'] + assert isinstance(samples, list) + assert len(samples) > 0 + for current_sample in samples: + assert type(current_sample) is str + assert result['ageAtDiagnosis'] >= min_age_at_diagnosis + + +def test_patients_query_with_passed_ethnicity(client, common_query, ethnicity): + response = client.post( + '/api', json={'query': common_query, 'variables': {'ethnicity': [ethnicity]}}) + json_data = json.loads(response.data) + page = json_data['data']['patients'] + results = page['items'] + + assert isinstance(results, list) + assert len(results) > 0 + for result in results: + assert result['ethnicity'] == ethnicity + + +def test_patients_query_with_passed_gender(client, common_query, gender): + response = client.post( + '/api', json={'query': common_query, 'variables': {'gender': [gender]}}) + json_data = json.loads(response.data) + page = json_data['data']['patients'] + results = page['items'] + + assert isinstance(results, list) + assert len(results) > 0 + for result in results: + assert result['gender'] == gender + + +def test_patients_query_with_passed_maxHeight(client, common_query, max_height): + response = client.post( + '/api', json={'query': common_query, 'variables': {'maxHeight': max_height}}) + json_data = json.loads(response.data) + page = json_data['data']['patients'] + results = page['items'] + + assert isinstance(results, list) + assert len(results) > 0 + for result in results: + assert result['height'] <= max_height + + +def test_patients_query_with_passed_minHeight(client, common_query, min_height): + response = client.post( + '/api', json={'query': common_query, 'variables': {'minHeight': min_height}}) + json_data = json.loads(response.data) + page = json_data['data']['patients'] + results = page['items'] + + assert isinstance(results, list) + assert len(results) > 0 + for result in results: + assert result['height'] >= min_height + + +def test_patients_query_with_passed_race(client, common_query, race): + response = client.post( + '/api', json={'query': common_query, 'variables': {'race': [race]}}) + json_data = json.loads(response.data) + page = json_data['data']['patients'] + results = page['items'] + + assert isinstance(results, list) + assert len(results) > 0 + for result in results: + assert result['race'] == race + + +def test_patients_query_with_passed_maxWeight(client, common_query, max_weight): + response = client.post( + '/api', json={'query': common_query, 'variables': {'maxWeight': max_weight}}) + json_data = json.loads(response.data) + page = json_data['data']['patients'] + results = page['items'] + + assert isinstance(results, list) + assert len(results) > 0 + for result in results: + assert result['weight'] <= max_weight + + +def test_patients_query_with_passed_minWeight(client, common_query, min_weight): + response = client.post( + '/api', json={'query': common_query, 'variables': {'minWeight': min_weight}}) + json_data = json.loads(response.data) + page = json_data['data']['patients'] + results = page['items'] + + assert isinstance(results, list) + assert len(results) > 0 + for result in results: + assert result['weight'] >= min_weight + + +def test_patients_query_with_passed_sample(client, full_query, sample): + response = client.post( + '/api', json={'query': full_query, 'variables': {'sample': [sample]}}) + json_data = json.loads(response.data) + page = json_data['data']['patients'] + results = page['items'] + + assert isinstance(results, list) + assert len(results) == 1 + for result in results: + samples = result['samples'] + assert isinstance(samples, list) + assert len(samples) == 1 + for current_sample in samples: + assert current_sample == sample + + +def test_patients_query_with_passed_slide(client, full_query, slide): + response = client.post( + '/api', json={'query': full_query, 'variables': {'slide': [slide]}}) + json_data = json.loads(response.data) + page = json_data['data']['patients'] + results = page['items'] + + assert isinstance(results, list) + assert len(results) > 0 + for result in results: + slides = result['slides'] + assert isinstance(slides, list) + assert len(slides) > 0 + for current_slide in slides: + assert current_slide['name'] == slide diff --git a/apps/iatlas/api-gitlab/tests/queries/test_rareVariantPathwayAssociation_query.py b/apps/iatlas/api-gitlab/tests/queries/test_rareVariantPathwayAssociation_query.py new file mode 100644 index 0000000000..cd7ee02ae1 --- /dev/null +++ b/apps/iatlas/api-gitlab/tests/queries/test_rareVariantPathwayAssociation_query.py @@ -0,0 +1,269 @@ +import json +import pytest +from tests import NoneType +from api.resolvers.resolver_helpers.paging_utils import from_cursor_hash, to_cursor_hash, Paging + + +@pytest.fixture(scope='module') +def rvpa_pathway(): + return 'MMR' + + +@pytest.fixture(scope='module') +def rvpa_max_p_value(): + return 0.495103 + + +@pytest.fixture(scope='module') +def rvpa_min_p_value(): + return 0.634187 + + +@pytest.fixture(scope='module') +def common_query_builder(): + def f(query_fields): + return """query RareVariantPathwayAssociation( + $paging: PagingInput + $distinct: Boolean + $dataSet: [String!] + $feature: [String!] + $pathway: [String!] + $minPValue: Float + $maxPValue: Float + ) { + rareVariantPathwayAssociations( + paging: $paging + distinct: $distinct + dataSet: $dataSet + feature: $feature + pathway: $pathway + minPValue: $minPValue + maxPValue: $maxPValue + )""" + query_fields + "}" + return f + + +@pytest.fixture(scope='module') +def common_query(common_query_builder): + return common_query_builder("""{ + items { + dataSet { name } + feature { + name + germlineModule + germlineCategory + } + pathway + pValue + min + max + mean + q1 + q2 + q3 + nMutants + nTotal + } + paging { + type + pages + total + startCursor + endCursor + hasPreviousPage + hasNextPage + page + limit + } + error + }""") + + +def test_rareVariantPathwayAssociation_cursor_pagination_first(client, common_query_builder): + query = common_query_builder("""{ + items { + id + } + paging { + type + pages + total + startCursor + endCursor + hasPreviousPage + hasNextPage + page + limit + } + }""") + num = 10 + response = client.post( + '/api', json={'query': query, 'variables': { + 'paging': {'first': num} + }}) + json_data = json.loads(response.data) + page = json_data['data']['rareVariantPathwayAssociations'] + items = page['items'] + paging = page['paging'] + start = from_cursor_hash(paging['startCursor']) + end = from_cursor_hash(paging['endCursor']) + + assert len(items) == num + assert paging['hasNextPage'] == True + assert paging['hasPreviousPage'] == False + assert start == items[0]['id'] + assert end == items[num - 1]['id'] + + +def test_rareVariantPathwayAssociation_cursor_pagination_last(client, common_query_builder): + query = common_query_builder("""{ + items { + id + } + paging { + type + pages + total + startCursor + endCursor + hasPreviousPage + hasNextPage + page + limit + } + }""") + num = 10 + response = client.post( + '/api', json={'query': query, 'variables': { + 'paging': { + 'last': num, + 'before': to_cursor_hash(1000) + } + }}) + json_data = json.loads(response.data) + page = json_data['data']['rareVariantPathwayAssociations'] + items = page['items'] + paging = page['paging'] + start = from_cursor_hash(paging['startCursor']) + end = from_cursor_hash(paging['endCursor']) + + assert len(items) == num + assert paging['hasNextPage'] == False + assert paging['hasPreviousPage'] == True + assert start == items[0]['id'] + assert end == items[num - 1]['id'] + + +def test_rareVariantPathwayAssociation_cursor_distinct_pagination(client, common_query): + page_num = 2 + num = 10 + response = client.post( + '/api', json={'query': common_query, 'variables': { + 'paging': { + 'page': page_num, + 'first': num, + }, + 'distinct': True, + 'dataSet': ['TCGA'] + }}) + json_data = json.loads(response.data) + page = json_data['data']['rareVariantPathwayAssociations'] + items = page['items'] + + assert len(items) == num + assert page_num == page['paging']['page'] + + +def test_rareVariantPathwayAssociation_query_with_passed_data_set_feature_and_pathway(client, common_query, data_set, rvpa_pathway, germline_feature, germline_category, germline_module): + response = client.post('/api', json={'query': common_query, 'variables': { + 'dataSet': [data_set], + 'feature': [germline_feature], + 'pathway': [rvpa_pathway] + }}) + json_data = json.loads(response.data) + page = json_data['data']['rareVariantPathwayAssociations'] + results = page['items'] + assert isinstance(results, list) + assert len(results) == 1 + for result in results: + assert result['dataSet']['name'] == data_set + assert result['feature']['name'] == germline_feature + assert result['feature']['germlineCategory'] == germline_category + assert result['feature']['germlineModule'] == germline_module + + assert result['pathway'] == rvpa_pathway + assert type(result['pValue']) is float + assert type(result['min']) is float + assert type(result['max']) is float + assert type(result['mean']) is float + assert type(result['q1']) is float + assert type(result['q2']) is float + assert type(result['q3']) is float + assert type(result['nMutants']) is int + assert type(result['nTotal']) is int + + +def test_rareVariantPathwayAssociation_query_with_passed_min_p_value(client, common_query, data_set, rvpa_min_p_value): + response = client.post( + '/api', json={'query': common_query, 'variables': { + 'dataSet': [data_set], + 'minPValue': rvpa_min_p_value + }}) + json_data = json.loads(response.data) + page = json_data['data']['rareVariantPathwayAssociations'] + results = page['items'] + assert isinstance(results, list) + assert len(results) > 0 + for result in results[0:2]: + assert type(result['pValue']) is float + assert result['pValue'] >= rvpa_min_p_value + + +def test_rareVariantPathwayAssociation_query_with_passed_max_p_value(client, common_query, data_set, rvpa_max_p_value): + response = client.post( + '/api', json={'query': common_query, 'variables': { + 'dataSet': [data_set], + 'maxPValue': rvpa_max_p_value + }}) + json_data = json.loads(response.data) + page = json_data['data']['rareVariantPathwayAssociations'] + results = page['items'] + assert isinstance(results, list) + assert len(results) > 0 + for result in results[0:2]: + assert type(result['pValue']) is float + assert result['pValue'] <= rvpa_max_p_value + + +def test_rareVariantPathwayAssociation_query_with_no_arguments_no_relations(client, common_query_builder): + query = common_query_builder("""{ + items { + pathway + pValue + min + max + mean + q1 + q2 + q3 + nMutants + nTotal + } + }""") + response = client.post('/api', json={'query': query}) + json_data = json.loads(response.data) + page = json_data['data']['rareVariantPathwayAssociations'] + results = page['items'] + assert isinstance(results, list) + assert len(results) > 0 + for result in results[0:2]: + assert type(result['pathway']) is str + assert type(result['pValue']) is float or NoneType + assert type(result['min']) is float or NoneType + assert type(result['max']) is float or NoneType + assert type(result['mean']) is float or NoneType + assert type(result['q1']) is float or NoneType + assert type(result['q2']) is float or NoneType + assert type(result['q3']) is float or NoneType + assert type(result['nMutants']) is int or NoneType + assert type(result['nTotal']) is int or NoneType diff --git a/apps/iatlas/api-gitlab/tests/queries/test_samples_query.py b/apps/iatlas/api-gitlab/tests/queries/test_samples_query.py new file mode 100644 index 0000000000..a5aeb1bddf --- /dev/null +++ b/apps/iatlas/api-gitlab/tests/queries/test_samples_query.py @@ -0,0 +1,250 @@ +import json +import pytest + + +@pytest.fixture(scope='module') +def common_query_builder(): + def f(query_fields): + return """query Samples( + $ethnicity: [EthnicityEnum!] + $gender: [GenderEnum!] + $maxAgeAtDiagnosis: Int + $maxHeight: Float + $maxWeight: Float + $minAgeAtDiagnosis: Int + $minHeight: Float + $minWeight: Float + $name: [String!] + $patient: [String!] + $race: [RaceEnum!] + ) { + samples( + ethnicity: $ethnicity + gender: $gender + maxAgeAtDiagnosis: $maxAgeAtDiagnosis + maxHeight: $maxHeight + maxWeight: $maxWeight + minAgeAtDiagnosis: $minAgeAtDiagnosis + minHeight: $minHeight + minWeight: $minWeight + name: $name + patient: $patient + race: $race + )""" + query_fields + "}" + return f + + +@pytest.fixture(scope='module') +def common_query(common_query_builder): + return common_query_builder( + """{ + items { + name + patient { + ageAtDiagnosis + barcode + ethnicity + gender + height + race + weight + } + } + paging { + type + pages + total + startCursor + endCursor + hasPreviousPage + hasNextPage + page + limit + } + error + }""" + ) + + +def test_samples_query_with_passed_sample(client, common_query, sample): + response = client.post( + '/api', json={'query': common_query, 'variables': {'name': [sample]}}) + json_data = json.loads(response.data) + page = json_data['data']['samples'] + results = page['items'] + + assert isinstance(results, list) + assert len(results) == 1 + for result in results[0:2]: + current_patient = result['patient'] + assert result['name'] == sample + if current_patient: + assert type(current_patient['barcode']) is str + + +def test_samples_query_with_passed_patient(client, common_query, patient): + response = client.post( + '/api', json={'query': common_query, 'variables': {'patient': [patient]}}) + json_data = json.loads(response.data) + page = json_data['data']['samples'] + results = page['items'] + + assert isinstance(results, list) + assert len(results) > 0 + for result in results[0:2]: + assert type(result['name']) is str + assert result['patient']['barcode'] == patient + + +def test_samples_query_with_passed_maxAgeAtDiagnosis(client, common_query, max_age_at_diagnosis): + response = client.post( + '/api', json={'query': common_query, 'variables': {'maxAgeAtDiagnosis': max_age_at_diagnosis}}) + json_data = json.loads(response.data) + page = json_data['data']['samples'] + results = page['items'] + + assert isinstance(results, list) + assert len(results) > 0 + for result in results[0:2]: + assert type(result['name']) is str + assert result['patient']['ageAtDiagnosis'] <= max_age_at_diagnosis + + +def test_samples_query_with_passed_minAgeAtDiagnosis(client, common_query, min_age_at_diagnosis): + response = client.post( + '/api', json={'query': common_query, 'variables': {'minAgeAtDiagnosis': min_age_at_diagnosis}}) + json_data = json.loads(response.data) + page = json_data['data']['samples'] + results = page['items'] + + assert isinstance(results, list) + assert len(results) > 0 + for result in results[0:2]: + assert type(result['name']) is str + assert result['patient']['ageAtDiagnosis'] >= min_age_at_diagnosis + + +def test_samples_query_with_passed_ethnicity(client, common_query, ethnicity): + response = client.post( + '/api', json={'query': common_query, 'variables': {'ethnicity': [ethnicity]}}) + json_data = json.loads(response.data) + page = json_data['data']['samples'] + results = page['items'] + + assert isinstance(results, list) + assert len(results) > 0 + for result in results[0:2]: + assert type(result['name']) is str + assert result['patient']['ethnicity'] == ethnicity + + +def test_samples_query_with_passed_gender(client, common_query, gender): + response = client.post( + '/api', json={'query': common_query, 'variables': {'gender': [gender]}}) + json_data = json.loads(response.data) + page = json_data['data']['samples'] + results = page['items'] + + assert isinstance(results, list) + assert len(results) > 0 + for result in results[0:2]: + assert type(result['name']) is str + assert result['patient']['gender'] == gender + + +def test_samples_query_with_passed_maxHeight(client, common_query, max_height): + response = client.post( + '/api', json={'query': common_query, 'variables': {'maxHeight': max_height}}) + json_data = json.loads(response.data) + page = json_data['data']['samples'] + results = page['items'] + + assert isinstance(results, list) + assert len(results) > 0 + for result in results[0:2]: + assert type(result['name']) is str + assert result['patient']['height'] <= max_height + + +def test_samples_query_with_passed_minHeight(client, common_query, min_height): + response = client.post( + '/api', json={'query': common_query, 'variables': {'minHeight': min_height}}) + json_data = json.loads(response.data) + page = json_data['data']['samples'] + results = page['items'] + + assert isinstance(results, list) + assert len(results) > 0 + for result in results[0:2]: + assert type(result['name']) is str + assert result['patient']['height'] >= min_height + + +def test_samples_query_with_passed_race(client, common_query, race): + response = client.post( + '/api', json={'query': common_query, 'variables': {'race': [race]}}) + json_data = json.loads(response.data) + page = json_data['data']['samples'] + results = page['items'] + + assert isinstance(results, list) + assert len(results) > 0 + for result in results[0:2]: + assert type(result['name']) is str + assert result['patient']['race'] == race + + +def test_samples_query_with_passed_maxWeight(client, common_query, max_weight): + response = client.post( + '/api', json={'query': common_query, 'variables': {'maxWeight': max_weight}}) + json_data = json.loads(response.data) + page = json_data['data']['samples'] + results = page['items'] + + assert isinstance(results, list) + assert len(results) > 0 + for result in results[0:2]: + assert type(result['name']) is str + assert result['patient']['weight'] <= max_weight + + +def test_samples_query_with_passed_minWeight(client, common_query, min_weight): + response = client.post( + '/api', json={'query': common_query, 'variables': {'minWeight': min_weight}}) + json_data = json.loads(response.data) + page = json_data['data']['samples'] + results = page['items'] + + assert isinstance(results, list) + assert len(results) > 0 + for result in results[0:2]: + assert type(result['name']) is str + assert result['patient']['weight'] >= min_weight + + +def test_samples_query_with_no_args(client, common_query): + response = client.post('/api', json={'query': common_query}) + json_data = json.loads(response.data) + page = json_data['data']['samples'] + results = page['items'] + + assert isinstance(results, list) + assert len(results) > 0 + for result in results[0:2]: + assert type(result['name']) is str + + +def test_samples_query_with_patient_and_sample(client, common_query, patient, sample): + response = client.post( + '/api', json={'query': common_query, 'variables': { + 'patient': [patient], + 'sample': [sample]}}) + json_data = json.loads(response.data) + page = json_data['data']['samples'] + results = page['items'] + + assert isinstance(results, list) + assert len(results) == 1 + for result in results[0:2]: + assert result['name'] == sample + assert result['patient']['barcode'] == patient diff --git a/apps/iatlas/api-gitlab/tests/queries/test_slides_query.py b/apps/iatlas/api-gitlab/tests/queries/test_slides_query.py new file mode 100644 index 0000000000..a696401e0c --- /dev/null +++ b/apps/iatlas/api-gitlab/tests/queries/test_slides_query.py @@ -0,0 +1,250 @@ +import json +import pytest +from api.database import return_slide_query +from tests import NoneType + + +@pytest.fixture(scope='module') +def common_query_builder(): + def f(query_fields): + return """query Slides( + $barcode: [String!] + $ethnicity: [EthnicityEnum!] + $gender: [GenderEnum!] + $maxAgeAtDiagnosis: Int + $maxHeight: Float + $maxWeight: Float + $minAgeAtDiagnosis: Int + $minHeight: Float + $minWeight: Float + $name: [String!] + $race: [RaceEnum!] + $sample: [String!] + ) { + slides( + barcode: $barcode + ethnicity: $ethnicity + gender: $gender + maxAgeAtDiagnosis: $maxAgeAtDiagnosis + maxHeight: $maxHeight + maxWeight: $maxWeight + minAgeAtDiagnosis: $minAgeAtDiagnosis + minHeight: $minHeight + minWeight: $minWeight + name: $name + race: $race + sample: $sample + )""" + query_fields + "}" + return f + + +@pytest.fixture(scope='module') +def common_query(common_query_builder): + return common_query_builder( + """{ + items { + id + name + description + patient { + ageAtDiagnosis + barcode + ethnicity + gender + height + race + weight + } + } + paging { + type + pages + total + startCursor + endCursor + hasPreviousPage + hasNextPage + page + limit + } + error + }""" + ) + + +def test_slides_query_with_passed_slide(client, common_query, slide): + response = client.post( + '/api', json={'query': common_query, 'variables': {'name': slide}}) + json_data = json.loads(response.data) + results = json_data['data']['slides']['items'] + + assert isinstance(results, list) + assert len(results) == 1 + for result in results: + assert result['name'] == slide + assert type(result['description']) is str or NoneType + assert type(result['patient']['barcode']) is str + + +def test_slides_query_with_passed_maxAgeAtDiagnosis(client, common_query, max_age_at_diagnosis): + response = client.post( + '/api', json={'query': common_query, 'variables': {'maxAgeAtDiagnosis': max_age_at_diagnosis}}) + json_data = json.loads(response.data) + results = json_data['data']['slides']['items'] + + assert isinstance(results, list) + assert len(results) > 0 + for result in results: + assert type(result['name']) is str + assert result['patient']['ageAtDiagnosis'] <= max_age_at_diagnosis + + +def test_slides_query_with_passed_minAgeAtDiagnosis(client, common_query, min_age_at_diagnosis): + response = client.post( + '/api', json={'query': common_query, 'variables': {'minAgeAtDiagnosis': min_age_at_diagnosis}}) + json_data = json.loads(response.data) + results = json_data['data']['slides']['items'] + + assert isinstance(results, list) + assert len(results) > 0 + for result in results: + assert type(result['name']) is str + assert result['patient']['ageAtDiagnosis'] >= min_age_at_diagnosis + + +def test_slides_query_with_passed_barcode(client, common_query, patient): + + response = client.post( + '/api', json={'query': common_query, 'variables': {'barcode': [patient]}}) + json_data = json.loads(response.data) + results = json_data['data']['slides']['items'] + + assert isinstance(results, list) + assert len(results) == 1 + for result in results: + assert type(result['name']) is str + assert result['patient']['barcode'] == patient + + +def test_slides_query_with_passed_ethnicity(client, common_query, ethnicity): + + response = client.post( + '/api', json={'query': common_query, 'variables': {'ethnicity': [ethnicity]}}) + json_data = json.loads(response.data) + results = json_data['data']['slides']['items'] + + assert isinstance(results, list) + assert len(results) > 0 + for result in results: + assert type(result['name']) is str + assert result['patient']['ethnicity'] == ethnicity + + +def test_slides_query_with_passed_gender(client, common_query, gender): + + response = client.post( + '/api', json={'query': common_query, 'variables': {'gender': [gender]}}) + json_data = json.loads(response.data) + results = json_data['data']['slides']['items'] + + assert isinstance(results, list) + assert len(results) > 0 + for result in results: + assert type(result['name']) is str + assert result['patient']['gender'] == gender + + +def test_slides_query_with_passed_maxHeight(client, common_query, max_height): + + response = client.post( + '/api', json={'query': common_query, 'variables': {'maxHeight': max_height}}) + json_data = json.loads(response.data) + results = json_data['data']['slides']['items'] + + assert isinstance(results, list) + assert len(results) > 0 + for result in results: + assert type(result['name']) is str + assert result['patient']['height'] <= max_height + + +def test_slides_query_with_passed_minHeight(client, common_query, min_height): + + response = client.post( + '/api', json={'query': common_query, 'variables': {'minHeight': min_height}}) + json_data = json.loads(response.data) + results = json_data['data']['slides']['items'] + + assert isinstance(results, list) + assert len(results) > 0 + for result in results: + assert type(result['name']) is str + assert result['patient']['height'] >= min_height + + +def test_slides_query_with_passed_race(client, common_query, race): + + response = client.post( + '/api', json={'query': common_query, 'variables': {'race': [race]}}) + json_data = json.loads(response.data) + results = json_data['data']['slides']['items'] + + assert isinstance(results, list) + assert len(results) > 0 + for result in results: + assert type(result['name']) is str + assert result['patient']['race'] == race + + +def test_slides_query_with_passed_maxWeight(client, common_query, max_weight): + + response = client.post( + '/api', json={'query': common_query, 'variables': {'maxWeight': max_weight}}) + json_data = json.loads(response.data) + results = json_data['data']['slides']['items'] + + assert isinstance(results, list) + assert len(results) > 0 + for result in results: + assert type(result['name']) is str + assert result['patient']['weight'] <= max_weight + + +def test_slides_query_with_passed_minWeight(client, common_query, min_weight): + + response = client.post( + '/api', json={'query': common_query, 'variables': {'minWeight': min_weight}}) + json_data = json.loads(response.data) + results = json_data['data']['slides']['items'] + + assert isinstance(results, list) + assert len(results) > 0 + for result in results: + assert type(result['name']) is str + assert result['patient']['weight'] >= min_weight + + +def test_slides_query_with_passed_sample(client, common_query, sample): + response = client.post( + '/api', json={'query': common_query, 'variables': {'sample': [sample]}}) + json_data = json.loads(response.data) + results = json_data['data']['slides']['items'] + + assert isinstance(results, list) + assert len(results) == 1 + for result in results: + assert type(result['name']) is str + + +def test_slides_query_no_args(client, common_query): + response = client.post( + '/api', json={'query': common_query}) + json_data = json.loads(response.data) + results = json_data['data']['slides']['items'] + + slide_count = return_slide_query('id').count() + + assert isinstance(results, list) + assert len(results) == slide_count + for result in results[0:1]: + assert type(result['name']) is str diff --git a/apps/iatlas/api-gitlab/tests/queries/test_snps_query.py b/apps/iatlas/api-gitlab/tests/queries/test_snps_query.py new file mode 100644 index 0000000000..1f44cd40df --- /dev/null +++ b/apps/iatlas/api-gitlab/tests/queries/test_snps_query.py @@ -0,0 +1,262 @@ +import json +import pytest +from tests import NoneType +from api.resolvers.resolver_helpers.paging_utils import from_cursor_hash, to_cursor_hash, Paging +from api.database import return_snp_query +""" +query Snps( + $paging: PagingInput + $distinct: Boolean + $name: [String!] + $rsid: [String!] + $chr: [String!] + $maxBP: Int + $minBP: Int +) { + snps( + paging: $paging + distinct: $distinct + dataSet: $dataSet + name: $name + rsid: $rsid + chr: $chr + maxBP: $maxBP + minBP: $minBP + ) { + paging { + type + pages + total + startCursor + endCursor + hasPreviousPage + hasNextPage + page + limit + } + error + items { + name + rsid + chr + bp + } + } +} +""" + + +@pytest.fixture(scope='module') +def snp_name(): + return '7:104003135:C:G' + + +@pytest.fixture(scope='module') +def snp_rsid(): + return 'rs2188491' + + +@pytest.fixture(scope='module') +def snp_chr(): + return '7' + + +@pytest.fixture(scope='module') +def max_bp(): + return 629418 + + +@pytest.fixture(scope='module') +def min_bp(): + return 245245741 + + +@pytest.fixture(scope='module') +def common_query_builder(): + def f(query_fields): + return """query Snps( + $paging: PagingInput + $distinct: Boolean + $name: [String!] + $rsid: [String!] + $chr: [String!] + $maxBP: Int + $minBP: Int + ) { + snps( + paging: $paging + distinct: $distinct + name: $name + rsid: $rsid + chr: $chr + maxBP: $maxBP + minBP: $minBP + )""" + query_fields + "}" + return f + + +@pytest.fixture(scope='module') +def common_query(common_query_builder): + return common_query_builder("""{ + items { + name + rsid + chr + bp + } + paging { + type + pages + total + startCursor + endCursor + hasPreviousPage + hasNextPage + page + limit + } + error + }""") + + +# Test that forward cursor pagination gives us the expected paginInfo + + +def test_snp_cursor_pagination_first(client, common_query_builder): + query = common_query_builder("""{ + items { + id + } + paging { + type + pages + total + startCursor + endCursor + hasPreviousPage + hasNextPage + page + limit + } + }""") + num = 10 + response = client.post( + '/api', json={'query': query, 'variables': { + 'paging': {'first': num} + }}) + json_data = json.loads(response.data) + page = json_data['data']['snps'] + items = page['items'] + paging = page['paging'] + start = from_cursor_hash(paging['startCursor']) + end = from_cursor_hash(paging['endCursor']) + + assert len(items) == num + assert paging['hasNextPage'] == True + assert paging['hasPreviousPage'] == False + assert type(items[0]['id']) is str + + +def test_snp_cursor_pagination_last(client, common_query_builder): + query = common_query_builder("""{ + items { + id + } + paging { + type + pages + total + startCursor + endCursor + hasPreviousPage + hasNextPage + page + limit + } + }""") + num = 10 + response = client.post( + '/api', json={'query': query, 'variables': { + 'paging': { + 'last': num, + 'before': to_cursor_hash(1000) + } + }}) + json_data = json.loads(response.data) + page = json_data['data']['snps'] + items = page['items'] + paging = page['paging'] + start = from_cursor_hash(paging['startCursor']) + end = from_cursor_hash(paging['endCursor']) + + assert len(items) == num + assert paging['hasNextPage'] == False + assert paging['hasPreviousPage'] == True + assert start == items[0]['id'] + assert end == items[num - 1]['id'] + + +def test_snp_cursor_distinct_pagination(client, common_query): + page_num = 1 + num = 10 + response = client.post( + '/api', json={'query': common_query, 'variables': { + 'paging': { + 'page': page_num, + 'first': num, + }, + 'distinct': True + }}) + json_data = json.loads(response.data) + page = json_data['data']['snps'] + items = page['items'] + + assert len(items) == num + assert page_num == page['paging']['page'] + + +def test_snps_query_with_passed_min_bp(client, common_query, min_bp): + response = client.post( + '/api', json={'query': common_query, 'variables': { + 'minBP': min_bp + }}) + json_data = json.loads(response.data) + page = json_data['data']['snps'] + results = page['items'] + assert isinstance(results, list) + assert len(results) > 0 + for result in results[0:2]: + assert result['bp'] >= min_bp + + +def test_snps_query_with_passed_max_bp(client, common_query, max_bp): + response = client.post( + '/api', json={'query': common_query, 'variables': { + 'maxBP': max_bp + }}) + json_data = json.loads(response.data) + page = json_data['data']['snps'] + results = page['items'] + assert isinstance(results, list) + assert len(results) > 0 + for result in results[0:2]: + assert result['bp'] <= max_bp + + +def test_snps_query_with_no_arguments(client, common_query_builder): + query = common_query_builder("""{ + items { + name + } + }""") + response = client.post('/api', json={'query': query}) + json_data = json.loads(response.data) + page = json_data['data']['snps'] + snp_results = page['items'] + # Get the total number of hr results in the database. + snp_count = return_snp_query('id').count() + + assert isinstance(snp_results, list) + assert len(snp_results) == snp_count + for snp_result in snp_results[0:2]: + assert type(snp_result['name']) is str diff --git a/apps/iatlas/api-gitlab/tests/queries/test_tags_query.py b/apps/iatlas/api-gitlab/tests/queries/test_tags_query.py new file mode 100644 index 0000000000..b71dfde17a --- /dev/null +++ b/apps/iatlas/api-gitlab/tests/queries/test_tags_query.py @@ -0,0 +1,532 @@ +import json +import pytest +from tests import NoneType +from api.resolvers.resolver_helpers.paging_utils import from_cursor_hash, to_cursor_hash + + +@pytest.fixture(scope='module') +def tag_with_publication(): + return 'BRCA_Normal' + + +@pytest.fixture(scope='module') +def tag_type(): + return 'group' + + +@pytest.fixture(scope='module') +def common_query_builder(): + def f(query_fields): + return """query Tags( + $cohort: [String!] + $dataSet: [String!] + $related: [String!] + $tag: [String!] + $sample: [String!] + $paging: PagingInput + $distinct: Boolean + $type: [TagTypeEnum!] + ) { + tags( + cohort: $cohort + dataSet: $dataSet + related: $related + tag: $tag + sample: $sample + type: $type + paging: $paging + distinct: $distinct + )""" + query_fields + "}" + return f + + +@pytest.fixture(scope='module') +def paging_query(common_query_builder): + query = common_query_builder("""{ + items { + id + } + paging { + type + pages + total + startCursor + endCursor + hasPreviousPage + hasNextPage + page + limit + } + }""") + return(query) + + +@pytest.fixture(scope='module') +def common_query(common_query_builder): + query = common_query_builder( + """ + { + items { + id + characteristics + color + longDisplay + name + shortDisplay + type + order + } + } + """ + ) + return(query) + + +@pytest.fixture(scope='module') +def samples_query(common_query_builder): + query = common_query_builder( + """ + { + items { + id + color + longDisplay + name + shortDisplay + characteristics + type + order + samples { name } + } + } + """ + ) + return(query) + + +@pytest.fixture(scope='module') +def related_query(common_query_builder): + query = common_query_builder( + """ + { + items { + id + characteristics + color + longDisplay + name + shortDisplay + type + order + related { + name + characteristics + color + longDisplay + shortDisplay + type + order + } + } + } + """ + ) + return(query) + + +@pytest.fixture(scope='module') +def publications_query(common_query_builder): + query = common_query_builder( + """ + { + items { + id + color + longDisplay + name + shortDisplay + characteristics + type + order + publications { + name + firstAuthorLastName + doId + journal + pubmedId + title + year + } + } + } + """ + ) + return(query) + + +@pytest.fixture(scope='module') +def sample_count_query(common_query_builder): + query = common_query_builder( + """ + { + items { + id + color + longDisplay + name + shortDisplay + characteristics + type + order + sampleCount + } + } + """ + ) + return(query) + + +@pytest.fixture(scope='module') +def full_query(common_query_builder): + query = common_query_builder( + """ + { + items { + id + color + longDisplay + name + shortDisplay + characteristics + type + order + related { + name + characteristics + color + longDisplay + shortDisplay + } + sampleCount + samples { name } + publications { name } + } + } + """ + ) + return(query) + + query = common_query_builder( + """ + { + items { + id + color + longDisplay + name + shortDisplay + characteristics + related { + name + characteristics + color + longDisplay + shortDisplay + } + sampleCount + publications { name } + } + } + """ + ) + return(query) + + +def test_tags_cursor_pagination_first(client, paging_query): + num = 5 + response = client.post( + '/api', json={'query': paging_query, 'variables': { + 'paging': {'first': num} + }}) + json_data = json.loads(response.data) + page = json_data['data']['tags'] + items = page['items'] + paging = page['paging'] + + start = from_cursor_hash(paging['startCursor']) + end = from_cursor_hash(paging['endCursor']) + assert len(items) == num + assert paging['hasNextPage'] == True + assert paging['hasPreviousPage'] == False + assert type(items[0]['id']) is str + + +def test_tags_cursor_pagination_last(client, paging_query): + num = 5 + response = client.post( + '/api', json={'query': paging_query, 'variables': { + 'paging': { + 'last': num, + 'before': to_cursor_hash(1000) + } + }}) + json_data = json.loads(response.data) + page = json_data['data']['tags'] + items = page['items'] + paging = page['paging'] + start = from_cursor_hash(paging['startCursor']) + end = from_cursor_hash(paging['endCursor']) + + assert len(items) == num + assert paging['hasNextPage'] == False + assert paging['hasPreviousPage'] == True + assert start == items[0]['id'] + assert end == items[num - 1]['id'] + + +def test_tags_cursor_distinct_pagination(client, paging_query): + page_num = 2 + num = 2 + response = client.post( + '/api', json={'query': paging_query, 'variables': { + 'paging': { + 'page': page_num, + 'first': num, + }, + 'distinct': True, + }}) + json_data = json.loads(response.data) + page = json_data['data']['tags'] + items = page['items'] + + assert len(items) == num + assert page_num == page['paging']['page'] + + +def test_tags_query_no_args(client, common_query): + num = 5 + response = client.post( + '/api', + json={ + 'query': common_query, + 'variables': {'paging': {'first': num}} + } + ) + json_data = json.loads(response.data) + page = json_data['data']['tags'] + results = page['items'] + assert isinstance(results, list) + assert len(results) == num + for result in results: + assert type(result['characteristics']) is str or NoneType + assert type(result['color']) is str or NoneType + assert type(result['longDisplay']) is str or NoneType + assert type(result['shortDisplay']) is str or NoneType + assert type(result['order']) is int or NoneType + assert type(result['name']) is str + assert type(result['type']) is str + assert 'sampleCount' not in result + assert 'samples' not in result + assert 'related' not in result + assert 'publications' not in result + + +def test_tags_query_with_data_set(client, common_query, data_set): + response = client.post( + '/api', + json={ + 'query': common_query, + 'variables': { + 'dataSet': [data_set] + } + } + ) + json_data = json.loads(response.data) + page = json_data['data']['tags'] + results = page['items'] + + assert isinstance(results, list) + assert len(results) == 3 + for result in results: + assert type(result['characteristics']) is str or NoneType + assert type(result['color']) is str or NoneType + assert type(result['longDisplay']) is str or NoneType + assert type(result['shortDisplay']) is str or NoneType + assert type(result['name']) is str + assert type(result['order']) is int or NoneType + assert type(result['type']) is str + + +def test_tags_query_with_tag_type(client, common_query, tag_type): + response = client.post( + '/api', + json={ + 'query': common_query, + 'variables': { + 'type': [tag_type] + } + } + ) + json_data = json.loads(response.data) + page = json_data['data']['tags'] + results = page['items'] + + assert isinstance(results, list) + assert len(results) > 0 + for result in results: + assert type(result['characteristics']) is str or NoneType + assert type(result['color']) is str or NoneType + assert type(result['longDisplay']) is str or NoneType + assert type(result['shortDisplay']) is str or NoneType + assert type(result['name']) is str + assert type(result['order']) is int or NoneType + assert result['type'] == tag_type + + +def test_tags_query_with_samples(client, samples_query): + response = client.post( + '/api', + json={ + 'query': samples_query, + 'variables': { + 'tag': ["C1"], + 'cohort': ["TCGA_Immune_Subtype"], + } + } + ) + json_data = json.loads(response.data) + page = json_data['data']['tags'] + results = page['items'] + + assert isinstance(results, list) + assert len(results) == 1 + for result in results: + assert result['name'] == "C1" + assert type(result['characteristics']) is str or NoneType + assert type(result['color']) is str or NoneType + assert type(result['longDisplay']) is str or NoneType + assert type(result['shortDisplay']) is str or NoneType + assert type(result['order']) is int or NoneType + assert isinstance(result['samples'], list) + assert len(result['samples']) > 1 + for sample in result['samples']: + assert type(sample['name']) is str + + +def test_tags_query_with_cohort(client, full_query, pcawg_cohort_name, pcawg_cohort_samples): + response = client.post( + '/api', + json={ + 'query': full_query, + 'variables': { + 'cohort': [pcawg_cohort_name] + } + } + ) + json_data = json.loads(response.data) + page = json_data['data']['tags'] + results = page['items'] + + assert isinstance(results, list) + assert len(results) > 1 + for result in results: + assert type(result['characteristics']) is str or NoneType + assert type(result['color']) is str or NoneType + assert type(result['longDisplay']) is str or NoneType + assert type(result['shortDisplay']) is str or NoneType + assert type(result['name']) is str + samples = result['samples'] + assert isinstance(samples, list) + assert result['sampleCount'] == len(samples) + for sample in samples: + assert type(sample['name']) is str + assert sample['name'] in pcawg_cohort_samples + + +def test_tags_query_with_related(client, related_query, related): + response = client.post( + '/api', + json={ + 'query': related_query, + 'variables': { + 'related': [related] + } + } + ) + json_data = json.loads(response.data) + page = json_data['data']['tags'] + results = page['items'] + + assert isinstance(results, list) + assert len(results) == 6 + for result in results: + assert type(result['characteristics']) is str or NoneType + assert type(result['color']) is str or NoneType + assert type(result['longDisplay']) is str or NoneType + assert type(result['shortDisplay']) is str or NoneType + assert type(result['name']) is str + assert type(result['order']) is int or NoneType + assert result['type'] == 'group' + assert result['name'] in ["C1", "C2", "C3", "C4", "C5", "C6"] + tags = result['related'] + assert isinstance(tags, list) + assert len(tags) == 1 + for tag in tags: + assert type(tag['characteristics']) is str or NoneType + assert type(tag['color']) is str or NoneType + assert type(tag['longDisplay']) is str or NoneType + assert type(tag['shortDisplay']) is str or NoneType + assert type(tag['name']) is str + assert tag['name'] == related + assert type(tag['order']) is int or NoneType + assert tag['type'] == 'parent_group' + + +def test_tags_query_with_sample(client, sample_count_query, sample): + response = client.post( + '/api', + json={ + 'query': sample_count_query, + 'variables': { + 'sample': [sample] + } + } + ) + json_data = json.loads(response.data) + page = json_data['data']['tags'] + results = page['items'] + + assert isinstance(results, list) + assert len(results) > 1 + + for result in results: + assert type(result['characteristics']) is str or NoneType + assert type(result['color']) is str or NoneType + assert type(result['longDisplay']) is str or NoneType + assert type(result['shortDisplay']) is str or NoneType + assert type(result['name']) is str + assert result['sampleCount'] == 1 + assert 'samples' not in result + + +def test_tags_query_returns_publications(client, publications_query, tag_with_publication): + response = client.post( + '/api', + json={ + 'query': publications_query, + 'variables': {'tag': [tag_with_publication]} + } + ) + json_data = json.loads(response.data) + page = json_data['data']['tags'] + results = page['items'] + + assert isinstance(results, list) + assert len(results) == 1 + for result in results: + publications = result['publications'] + assert result['name'] == tag_with_publication + assert isinstance(publications, list) + assert len(publications) > 0 + for publication in publications[0:5]: + assert type(publication['name']) is str diff --git a/apps/iatlas/api-gitlab/tests/queries/test_test_query.py b/apps/iatlas/api-gitlab/tests/queries/test_test_query.py new file mode 100644 index 0000000000..036afd8e09 --- /dev/null +++ b/apps/iatlas/api-gitlab/tests/queries/test_test_query.py @@ -0,0 +1,31 @@ +import json +import pytest + + +def test_test_query(client): + query = """query Test { + test { + items { + contentType + userAgent + headers { + contentLength + contentType + host + userAgent + } + } + page + } + }""" + response = client.post('/api', json={'query': query}) + json_data = json.loads(response.data) + test = json_data['data']['test'] + results = test['items'] + + assert type(results['contentType']) is str + assert type(results['userAgent']) is str + assert type(results['headers']['contentLength']) is int + assert type(results['headers']['contentType']) is str + assert type(results['headers']['host']) is str + assert type(results['headers']['userAgent']) is str diff --git a/apps/iatlas/api-gitlab/tests/test_config.py b/apps/iatlas/api-gitlab/tests/test_config.py new file mode 100644 index 0000000000..e283a780a6 --- /dev/null +++ b/apps/iatlas/api-gitlab/tests/test_config.py @@ -0,0 +1,73 @@ +import pytest +from _pytest.monkeypatch import MonkeyPatch +from os import getenv +from config import get_config, get_database_uri + + +def test_get_database_uri(monkeypatch: MonkeyPatch): + monkeypatch.setenv('POSTGRES_USER', 'TestingUser') + monkeypatch.setenv('POSTGRES_PASSWORD', 'TestingPassword') + monkeypatch.setenv('POSTGRES_DB', 'TestingDB') + monkeypatch.setenv('POSTGRES_HOST', 'TestingHost') + + monkeypatch.delenv('POSTGRES_PORT', raising=False) + assert get_database_uri() == 'postgresql://TestingUser:TestingPassword@TestingHost/TestingDB' + + monkeypatch.setenv('POSTGRES_PORT', '4242') + assert get_database_uri( + ) == 'postgresql://TestingUser:TestingPassword@TestingHost:4242/TestingDB' + + DATABASE_URI = 'postgresql://SomeUser:SomePassword@SomeHost/SomeDB' + monkeypatch.setenv('DATABASE_URI', DATABASE_URI) + assert get_database_uri() == DATABASE_URI + + +def test_testing_config(app): + FLASK_ENV = getenv('FLASK_ENV') + if FLASK_ENV == 'development': + assert app.config['DEBUG'] + else: + assert not app.config['DEBUG'] + assert not app.config['PROFILE'] + assert app.config['TESTING'] + assert app.config['SQLALCHEMY_DATABASE_URI'] == get_database_uri() + assert app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] == False + + +def test_development_config(monkeypatch: MonkeyPatch): + from api import create_app + + FLASK_ENV = 'development' + monkeypatch.setenv('FLASK_ENV', FLASK_ENV) + app = create_app() + assert app.config['DEBUG'] + assert app.config['PROFILE'] + assert not app.config['TESTING'] + assert app.config['SQLALCHEMY_DATABASE_URI'] == get_database_uri() + assert app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] == False + + +def test_staging_config(monkeypatch: MonkeyPatch): + from api import create_app + + FLASK_ENV = 'staging' + monkeypatch.setenv('FLASK_ENV', FLASK_ENV) + app = create_app() + assert not app.config['DEBUG'] + assert not app.config['PROFILE'] + assert not app.config['TESTING'] + assert app.config['SQLALCHEMY_DATABASE_URI'] == get_database_uri() + assert app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] == False + + +def test_production_config(monkeypatch: MonkeyPatch): + from api import create_app + + FLASK_ENV = 'production' + monkeypatch.setenv('FLASK_ENV', FLASK_ENV) + app = create_app() + assert not app.config['DEBUG'] + assert not app.config['PROFILE'] + assert not app.config['TESTING'] + assert app.config['SQLALCHEMY_DATABASE_URI'] == get_database_uri() + assert app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] == False diff --git a/apps/iatlas/api-gitlab/tests/test_database_helpers.py b/apps/iatlas/api-gitlab/tests/test_database_helpers.py new file mode 100644 index 0000000000..e5b5b50620 --- /dev/null +++ b/apps/iatlas/api-gitlab/tests/test_database_helpers.py @@ -0,0 +1,73 @@ +import pytest +from sqlalchemy import orm +from sqlalchemy.dialects import postgresql +from api.database.database_helpers import ( + build_general_query, build_option_args, build_query_args) +from api.db_models import Base, Gene +from api import db + + +class MockModel(Base): + id = db.Column(db.Integer, primary_key=True) + name = db.Column(db.String, nullable=False) + + def __repr__(self): + return '' % self.id + + +def test_build_general_query(db_session): + model = Gene + query_arg_1 = 'id' + query_arg_2 = 'entrez_id' + accepted_query_args = [query_arg_1, query_arg_2] + option_value_1 = 'gene_sets' + accepted_option_args = [option_value_1] + test_1 = build_general_query( + model, args=[query_arg_1, + query_arg_2, option_value_1], accepted_option_args=accepted_option_args, + accepted_query_args=accepted_query_args) + test_2 = build_general_query( + model, args=[query_arg_1, + query_arg_2], accepted_option_args=accepted_option_args, + accepted_query_args=accepted_query_args) + test_3 = build_general_query( + model, args=[], accepted_option_args=accepted_option_args, + accepted_query_args=accepted_query_args) + + assert str(test_1.statement.compile(dialect=postgresql.dialect())) == str(db_session.query(model).options( + orm.joinedload(option_value_1)).statement.compile(dialect=postgresql.dialect())) + + assert str(test_2.statement.compile(dialect=postgresql.dialect())) == str( + db_session.query(getattr(model, query_arg_1), getattr(model, query_arg_2)).statement.compile(dialect=postgresql.dialect())) + + assert str(test_3.statement.compile(dialect=postgresql.dialect())) == str( + db_session.query(model).statement.compile(dialect=postgresql.dialect())) + + +def test_build_option_args(): + expected_value_1 = 'nice' + expected_value_2 = 'good' + accepted_args = [expected_value_1, expected_value_2] + test_1 = build_option_args( + expected_value_1, accepted_args=accepted_args) + test_2 = build_option_args( + expected_value_1, expected_value_2, accepted_args=accepted_args) + assert test_1 and isinstance(test_1, list) + assert len(test_1) == 1 + assert test_2 and isinstance(test_2, list) + assert len(test_2) == 2 + assert not build_option_args(expected_value_1) + assert not build_option_args(expected_value_1, []) + + +def test_build_query_args(): + arg_1 = 'id' + arg_2 = 'name' + accepted_args = [arg_1, arg_2] + test_1 = build_query_args(MockModel, arg_1, arg_2, + accepted_args=accepted_args) + test_2 = build_query_args(MockModel, arg_1, arg_2) + test_3 = build_query_args(MockModel) + assert test_1 == [MockModel.id, MockModel.name] + assert test_2 == [MockModel] + assert test_3 == [MockModel] diff --git a/apps/iatlas/api-gitlab/tests/test_resolver_helpers.py b/apps/iatlas/api-gitlab/tests/test_resolver_helpers.py new file mode 100644 index 0000000000..b1763e4ca8 --- /dev/null +++ b/apps/iatlas/api-gitlab/tests/test_resolver_helpers.py @@ -0,0 +1,51 @@ +import pytest +from api.resolvers.resolver_helpers import build_option_args, get_value + + +class Parent: + def __init__(self, value, value2): + self.name = value + self.other = value2 + + +class MockSelectionSet: + def __init__(self, selections): + self.selections = selections + + +class MockSelection: + def __init__(self, name): + self.name = name + + +class MockNameNode: + def __init__(self, value): + self.value = value + + +def test_build_option_args(): + valid_nodes_1 = {'test1': 'nice', + 'test2': 'good'} + valid_nodes_2 = {'test0': 'oh', + 'test2': 'good'} + selection_set = MockSelectionSet([ + MockSelection(MockNameNode('test1')), + MockSelection(MockNameNode('test2')) + ]) + assert build_option_args(selection_set, valid_nodes_1) == {'nice', 'good'} + assert build_option_args(selection_set, valid_nodes_2) == {'good'} + assert build_option_args(None, valid_nodes_1) == set() + assert build_option_args(selection_set, {}) == set() + assert build_option_args(selection_set) == set() + assert build_option_args() == set() + + +def test_get_value(): + name = 'test' + other = 'test2' + parent = Parent(name, other) + assert get_value(parent, 'name') == name + assert get_value(parent, 'nothing') == None + assert get_value(parent, 'other') == other + assert get_value(None) == None + assert get_value() == None diff --git a/apps/iatlas/api-gitlab/tests/test_routes.py b/apps/iatlas/api-gitlab/tests/test_routes.py new file mode 100644 index 0000000000..f727fd8073 --- /dev/null +++ b/apps/iatlas/api-gitlab/tests/test_routes.py @@ -0,0 +1,35 @@ +import json +import pytest + + +def test_graphiql_get(client): + response = client.get('/graphiql') + assert response.status_code == 200 + + +def test_healthcheck_get(client): + response = client.get('/healthcheck') + assert response.status_code == 200 + + +def test_unknown_get(client): + response = client.get('/not_real') + assert response.status_code == 404 + + +def test_graphiql_post(client): + query = """query Test { test { items { userAgent } } }""" + response = client.post('/graphiql', json={'query': query}) + json_data = json.loads(response.data) + user_agent = json_data['data']['test']['items']['userAgent'] + + assert type(user_agent) is str + + +def test_api_post(client): + query = """query Test { test { items { userAgent } } }""" + response = client.post('/api', json={'query': query}) + json_data = json.loads(response.data) + user_agent = json_data['data']['test']['items']['userAgent'] + + assert type(user_agent) is str diff --git a/apps/iatlas/api-gitlab/unset_env_variables.sh b/apps/iatlas/api-gitlab/unset_env_variables.sh new file mode 100755 index 0000000000..b251d0461e --- /dev/null +++ b/apps/iatlas/api-gitlab/unset_env_variables.sh @@ -0,0 +1,23 @@ +#!/bin/bash + +# Defined some useful colors for echo outputs. +# Use Green for a successful action. +GREEN="\033[0;32m" +# No Color (used to stop or reset a color). +NC='\033[0m' + + +# Unset any previously set environment variables. +unset DOT_ENV_FILE +unset FLASK_APP +unset FLASK_ENV +unset FLASK_RUN_PORT +unset POSTGRES_DB +unset POSTGRES_HOST +unset POSTGRES_PORT +unset POSTGRES_PASSWORD +unset POSTGRES_USER +unset PYTHON_IMAGE_VERSION +unset SNAKEVIZ_PORT + +>&2 echo -e "${GREEN}* Environment variables unset.${NC}" \ No newline at end of file diff --git a/apps/iatlas/api-gitlab/uwsgi.ini b/apps/iatlas/api-gitlab/uwsgi.ini new file mode 100644 index 0000000000..528641f4be --- /dev/null +++ b/apps/iatlas/api-gitlab/uwsgi.ini @@ -0,0 +1,4 @@ +[uwsgi] +module = iatlasapi +callable = app +enable-threads = true \ No newline at end of file diff --git a/apps/iatlas/api-gitlab/view_profile.sh b/apps/iatlas/api-gitlab/view_profile.sh new file mode 100755 index 0000000000..d67b08f719 --- /dev/null +++ b/apps/iatlas/api-gitlab/view_profile.sh @@ -0,0 +1,31 @@ +#!/bin/bash + +GREEN="\033[0;32m" +YELLOW="\033[1;33m" +# No Color +NC='\033[0m' + +# The project directory. +PROJECT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null 2>&1 && pwd )" +>&2 echo -e "${GREEN}Current project dir - ${PROJECT_DIR}${NC}" + +unset options i +while IFS= read -r -d $'\0' f; do + options[i++]="$f" +done < <(find ${PROJECT_DIR}/.profiles -maxdepth 1 -type f -name "*.profile" -print0 ) +select opt in "${options[@]}" "Quit"; do + case $opt in + *.profile) + echo "Profile file $opt selected" + snakeviz -H 0.0.0.0 -p ${SNAKEVIZ_PORT} -s $opt + # processing + ;; + "Quit") + echo "Exiting..." + break + ;; + *) + echo "This is not a number" + ;; + esac +done \ No newline at end of file