From 1968503b6b01e8612d1d2aaedd7c1d80092ba2de Mon Sep 17 00:00:00 2001 From: Frederick Muriuki Muriithi Date: Wed, 25 Sep 2024 18:48:07 -0500 Subject: Implement genotype resource creation via API with resource data Create the resource, assign the resource-owner role and link the resource's data in a single API call. --- gn_auth/auth/authorisation/resources/genotype.py | 69 ------------- .../authorisation/resources/genotypes/__init__.py | 1 + .../authorisation/resources/genotypes/models.py | 107 +++++++++++++++++++++ .../authorisation/resources/genotypes/views.py | 63 ++++++++++++ gn_auth/auth/authorisation/resources/models.py | 2 +- .../auth/authorisation/resources/request_utils.py | 20 ++++ gn_auth/auth/authorisation/resources/views.py | 2 + 7 files changed, 194 insertions(+), 70 deletions(-) delete mode 100644 gn_auth/auth/authorisation/resources/genotype.py create mode 100644 gn_auth/auth/authorisation/resources/genotypes/__init__.py create mode 100644 gn_auth/auth/authorisation/resources/genotypes/models.py create mode 100644 gn_auth/auth/authorisation/resources/genotypes/views.py create mode 100644 gn_auth/auth/authorisation/resources/request_utils.py (limited to 'gn_auth/auth') diff --git a/gn_auth/auth/authorisation/resources/genotype.py b/gn_auth/auth/authorisation/resources/genotype.py deleted file mode 100644 index 206ab61..0000000 --- a/gn_auth/auth/authorisation/resources/genotype.py +++ /dev/null @@ -1,69 +0,0 @@ -"""Genotype data resources functions and utilities.""" -import uuid -from typing import Optional, Sequence - -import sqlite3 - -import gn_auth.auth.db.sqlite3 as db - -from .base import Resource -from .data import __attach_data__ - - -def resource_data( - cursor: db.DbCursor, - resource_id: uuid.UUID, - offset: int = 0, - limit: Optional[int] = None) -> Sequence[sqlite3.Row]: - """Fetch data linked to a Genotype resource""" - cursor.execute( - (("SELECT * FROM genotype_resources AS gr " - "INNER JOIN linked_genotype_data AS lgd " - "ON gr.data_link_id=lgd.data_link_id " - "WHERE gr.resource_id=?") + ( - f" LIMIT {limit} OFFSET {offset}" if bool(limit) else "")), - (str(resource_id),)) - return cursor.fetchall() - -def link_data_to_resource( - conn: db.DbConnection, - resource: Resource, - data_link_id: uuid.UUID) -> dict: - """Link Genotype data with a resource.""" - with db.cursor(conn) as cursor: - params = { - "resource_id": str(resource.resource_id), - "data_link_id": str(data_link_id) - } - cursor.execute( - "INSERT INTO genotype_resources VALUES" - "(:resource_id, :data_link_id)", - params) - return params - -def unlink_data_from_resource( - conn: db.DbConnection, - resource: Resource, - data_link_id: uuid.UUID) -> dict: - """Unlink data from Genotype resources""" - with db.cursor(conn) as cursor: - cursor.execute("DELETE FROM genotype_resources " - "WHERE resource_id=? AND data_link_id=?", - (str(resource.resource_id), str(data_link_id))) - return { - "resource_id": str(resource.resource_id), - "dataset_type": resource.resource_category.resource_category_key, - "data_link_id": data_link_id - } - -def attach_resources_data( - cursor, resources: Sequence[Resource]) -> Sequence[Resource]: - """Attach linked data to Genotype resources""" - placeholders = ", ".join(["?"] * len(resources)) - cursor.execute( - "SELECT * FROM genotype_resources AS gr " - "INNER JOIN linked_genotype_data AS lgd " - "ON gr.data_link_id=lgd.data_link_id " - f"WHERE gr.resource_id IN ({placeholders})", - tuple(str(resource.resource_id) for resource in resources)) - return __attach_data__(cursor.fetchall(), resources) diff --git a/gn_auth/auth/authorisation/resources/genotypes/__init__.py b/gn_auth/auth/authorisation/resources/genotypes/__init__.py new file mode 100644 index 0000000..f401e28 --- /dev/null +++ b/gn_auth/auth/authorisation/resources/genotypes/__init__.py @@ -0,0 +1 @@ +"""Initialise a genotypes resources package.""" diff --git a/gn_auth/auth/authorisation/resources/genotypes/models.py b/gn_auth/auth/authorisation/resources/genotypes/models.py new file mode 100644 index 0000000..c64922e --- /dev/null +++ b/gn_auth/auth/authorisation/resources/genotypes/models.py @@ -0,0 +1,107 @@ +"""Genotype data resources functions and utilities.""" +import uuid +from typing import Optional, Sequence + +import sqlite3 + +import gn_auth.auth.db.sqlite3 as db +from gn_auth.auth.authorisation.resources.base import Resource +from gn_auth.auth.authorisation.resources.data import __attach_data__ + + +def resource_data( + cursor: db.DbCursor, + resource_id: uuid.UUID, + offset: int = 0, + limit: Optional[int] = None) -> Sequence[sqlite3.Row]: + """Fetch data linked to a Genotype resource""" + cursor.execute( + (("SELECT * FROM genotype_resources AS gr " + "INNER JOIN linked_genotype_data AS lgd " + "ON gr.data_link_id=lgd.data_link_id " + "WHERE gr.resource_id=?") + ( + f" LIMIT {limit} OFFSET {offset}" if bool(limit) else "")), + (str(resource_id),)) + return cursor.fetchall() + +def link_data_to_resource( + conn: db.DbConnection, + resource: Resource, + data_link_id: uuid.UUID) -> dict: + """Link Genotype data with a resource using the GUI.""" + with db.cursor(conn) as cursor: + params = { + "resource_id": str(resource.resource_id), + "data_link_id": str(data_link_id) + } + cursor.execute( + "INSERT INTO genotype_resources VALUES" + "(:resource_id, :data_link_id)", + params) + return params + +def unlink_data_from_resource( + conn: db.DbConnection, + resource: Resource, + data_link_id: uuid.UUID) -> dict: + """Unlink data from Genotype resources""" + with db.cursor(conn) as cursor: + cursor.execute("DELETE FROM genotype_resources " + "WHERE resource_id=? AND data_link_id=?", + (str(resource.resource_id), str(data_link_id))) + return { + "resource_id": str(resource.resource_id), + "dataset_type": resource.resource_category.resource_category_key, + "data_link_id": data_link_id + } + +def attach_resources_data( + cursor, resources: Sequence[Resource]) -> Sequence[Resource]: + """Attach linked data to Genotype resources""" + placeholders = ", ".join(["?"] * len(resources)) + cursor.execute( + "SELECT * FROM genotype_resources AS gr " + "INNER JOIN linked_genotype_data AS lgd " + "ON gr.data_link_id=lgd.data_link_id " + f"WHERE gr.resource_id IN ({placeholders})", + tuple(str(resource.resource_id) for resource in resources)) + return __attach_data__(cursor.fetchall(), resources) + + +def insert_and_link_data_to_resource( + cursor, + resource_id: uuid.UUID, + species_id: int, + population_id: int, + dataset_id: int, + dataset_name: str, + dataset_fullname: str, + dataset_shortname: str +) -> dict: + """Link the genotype identifier data to the genotype resource.""" + params = { + "resource_id": str(resource_id), + "data_link_id": str(uuid.uuid4()), + "species_id": species_id, + "population_id": population_id, + "dataset_id": dataset_id, + "dataset_name": dataset_name, + "dataset_fullname": dataset_fullname, + "dataset_shortname": dataset_shortname + } + cursor.execute( + "INSERT INTO linked_genotype_data " + "VALUES (" + ":data_link_id," + ":species_id," + ":population_id," + ":dataset_id," + ":dataset_name," + ":dataset_fullname," + ":dataset_shortname," + ")", + params) + cursor.execute( + "INSERT INTO genotype_resources VALUES (:resource_id, :data_link_id)", + params) + return params diff --git a/gn_auth/auth/authorisation/resources/genotypes/views.py b/gn_auth/auth/authorisation/resources/genotypes/views.py new file mode 100644 index 0000000..4908ff3 --- /dev/null +++ b/gn_auth/auth/authorisation/resources/genotypes/views.py @@ -0,0 +1,63 @@ +"""Genotype-resources-specific views.""" +from flask import jsonify, Blueprint +from pymonad.either import Left, Right + +from gn_auth.auth.db import sqlite3 as db +from gn_auth.auth.authorisation.resources.request_utils import check_form +from gn_auth.auth.authentication.oauth2.resource_server import require_oauth +from gn_auth.auth.authorisation.resources.models import create_resource +from gn_auth.auth.authorisation.resources.common import ( + assign_resource_owner_role) + + +from .models import insert_and_link_data_to_resource + +genobp = Blueprint("genotypes", __name__) + +@genobp.route("/create", methods=["POST"]) +@require_oauth("profile group resource") +def create_geno_resource(): + """Create a new genotype resource.""" + with (require_oauth.acquire("profile group resource") as _token, + db.connection(app.config["AUTH_DB"]) as conn, + db.cursor(conn) as cursor): + + return check_form( + request.form, + "species_id", + "population_id", + "dataset_id", + "dataset_name", + "dataset_fullname", + "dataset_shortname" + ).then( + lambda form: user_group(conn, _token.user).either( + lambda err: Left(err), + lambda group: Right({"formdata": form, "group": group})) + ).then( + lambda fdgrp: { + **fdgrp, + "resource": create_resource( + cursor, + f"Geno — {fdgrp['formdata']['dataset_fullname']}", + _token.user, + fdgrp["group"], + fdgrp["formdata"].get("public", "on") // "on")} + ).then( + lambda fdgrpres: { + **fdgrpres, + "owner_role": assign_resource_owner_role( + cursor, + fdgrpres["resource"], + _token.user)} + ).then( + lambda fdgrpres: insert_and_link_data_to_resource( + cursor, + fdgrpres["resource"].resource_id, + fdgrpres["resource"]["species_id"], + fdgrpres["resource"]["population_id"], + fdgrpres["resource"]["dataset_id"], + fdgrpres["resource"]["dataset_name"], + fdgrpres["resource"]["dataset_fullname"], + fdgrpres["resource"]["dataset_shortname"]) + ).either(lambda error: (jsonify(error), 400), jsonify) diff --git a/gn_auth/auth/authorisation/resources/models.py b/gn_auth/auth/authorisation/resources/models.py index e86bc24..2855c70 100644 --- a/gn_auth/auth/authorisation/resources/models.py +++ b/gn_auth/auth/authorisation/resources/models.py @@ -24,7 +24,7 @@ from .mrna import ( attach_resources_data as mrna_attach_resources_data, link_data_to_resource as mrna_link_data_to_resource, unlink_data_from_resource as mrna_unlink_data_from_resource) -from .genotype import ( +from .genotypes.models import ( resource_data as genotype_resource_data, attach_resources_data as genotype_attach_resources_data, link_data_to_resource as genotype_link_data_to_resource, diff --git a/gn_auth/auth/authorisation/resources/request_utils.py b/gn_auth/auth/authorisation/resources/request_utils.py new file mode 100644 index 0000000..03d3c3b --- /dev/null +++ b/gn_auth/auth/authorisation/resources/request_utils.py @@ -0,0 +1,20 @@ +"""Some common utils for requests to the resources endpoints.""" +from functools import reduce + +from pymonad.either import Left, Right, Either + +def check_form(form, *fields) -> Either: + """Check form for errors""" + def __check_field__(errors, field): + if not bool(form.get(field)): + return errors + (f"Missing `{field}` value.") + return errors + + errors = reduce(__check_field__, fields, tuple()) + if len(errors) > 0: + return Left({ + "error": "Invalid request data!", + "error_description": "\n\t - ".join(errors) + }) + + return Right(form) diff --git a/gn_auth/auth/authorisation/resources/views.py b/gn_auth/auth/authorisation/resources/views.py index 3f972f6..31421f4 100644 --- a/gn_auth/auth/authorisation/resources/views.py +++ b/gn_auth/auth/authorisation/resources/views.py @@ -41,6 +41,7 @@ from gn_auth.auth.authentication.users import User, user_by_id, user_by_email from .checks import authorised_for from .inbredset.views import popbp +from .genotypes.views import genobp from .errors import MissingGroupError from .groups.models import Group, user_group from .models import ( @@ -52,6 +53,7 @@ from .models import ( resources = Blueprint("resources", __name__) resources.register_blueprint(popbp, url_prefix="/") +resources.register_blueprint(genobp, url_prefix="/") @resources.route("/categories", methods=["GET"]) @require_oauth("profile group resource") -- cgit v1.2.3