diff options
| -rw-r--r-- | gn3/api/case_attributes.py | 69 | ||||
| -rw-r--r-- | gn3/db/case_attributes.py | 65 | ||||
| -rw-r--r-- | gn3/db/sample_data.py | 4 | ||||
| -rw-r--r-- | tests/unit/db/test_case_attributes.py | 2 |
4 files changed, 59 insertions, 81 deletions
diff --git a/gn3/api/case_attributes.py b/gn3/api/case_attributes.py index 536d74d..5662bfe 100644 --- a/gn3/api/case_attributes.py +++ b/gn3/api/case_attributes.py @@ -1,5 +1,6 @@ """Implement case-attribute manipulations.""" from typing import Union +from pathlib import Path from functools import reduce from urllib.parse import urljoin @@ -181,21 +182,22 @@ def inbredset_case_attribute_values(inbredset_id: int) -> Response: return jsonify(__case_attribute_values_by_inbred_set__(conn, inbredset_id)) +# pylint: disable=[too-many-locals] @caseattr.route("/<int:inbredset_id>/edit", methods=["POST"]) @require_token -def edit_case_attributes(inbredset_id: int, auth_token=None) -> Response: +def edit_case_attributes(inbredset_id: int, auth_token=None) -> tuple[Response, int]: """Edit the case attributes for `InbredSetId` based on data received. :inbredset_id: Identifier for the population that the case attribute belongs :auth_token: A validated JWT from the auth server """ with database_connection(current_app.config["SQL_URI"]) as conn, conn.cursor() as cursor: - data = request.json["edit-data"] + data = request.json["edit-data"] # type: ignore modified = { "inbredset_id": inbredset_id, "Modifications": {}, } - original, current = {}, {} + original, current = {}, {} # type: ignore for key, value in data.items(): strain, case_attribute = key.split(":") @@ -205,16 +207,18 @@ def edit_case_attributes(inbredset_id: int, auth_token=None) -> Response: if not original.get(strain): original[strain] = {} original[strain][case_attribute] = value["Original"] - modified["Modifications"]["Original"] = original - modified["Modifications"]["Current"] = current + modified["Modifications"]["Original"] = original # type: ignore + modified["Modifications"]["Current"] = current # type: ignore edit = CaseAttributeEdit( inbredset_id=inbredset_id, status=EditStatus.review, user_id=auth_token["jwt"]["sub"], changes=modified ) + directory = (Path(current_app.config["LMDB_DATA_PATH"]) / + "case-attributes" / str(inbredset_id)) _id = queue_edit(cursor=cursor, - directory=current_app.config["LMDB_DATA_PATH"], + directory=directory, edit=edit) try: required_access(auth_token, @@ -223,31 +227,31 @@ def edit_case_attributes(inbredset_id: int, auth_token=None) -> Response: "system:inbredset:apply-case-attribute-edit")) match apply_change( cursor, change_type=EditStatus.approved, - change_id=_id, - directory=current_app.config["LMDB_DATA_PATH"] + change_id=_id, # type: ignore + directory=directory ): case True: return jsonify({ "diff-status": "applied", "message": ("The changes to the case-attributes have been " "applied successfully.") - }) + }), 201 case _: return jsonify({ "diff-status": "no changes to be applied", "message": ("There were no changes to be made ") - }) + }), 200 except AuthorisationError as _auth_err: return jsonify({ "diff-status": "queued", "message": ("The changes to the case-attributes have been " "queued for approval."), - }), 401 + }), 201 @caseattr.route("/<int:inbredset_id>/diff/list", methods=["GET"]) @require_token -def list_diffs(inbredset_id: int, auth_token=None) -> Response: +def list_diffs(inbredset_id: int, auth_token=None) -> tuple[Response, int]: """List any changes that have not been approved/rejected.""" try: required_access(auth_token, @@ -256,9 +260,9 @@ def list_diffs(inbredset_id: int, auth_token=None) -> Response: "system:inbredset:apply-case-attribute-edit")) with (database_connection(current_app.config["SQL_URI"]) as conn, conn.cursor(cursorclass=DictCursor) as cursor): - changes = get_changes(cursor, inbredset_id=inbredset_id, - directory=current_app.config["LMDB_DATA_PATH"]) - current_app.logger.error(changes) + directory = (Path(current_app.config["LMDB_DATA_PATH"]) / + "case-attributes" / str(inbredset_id)) + changes = get_changes(cursor, directory=directory) return jsonify( changes ), 200 @@ -270,27 +274,32 @@ def list_diffs(inbredset_id: int, auth_token=None) -> Response: @caseattr.route("/<int:inbredset_id>/approve/<int:change_id>", methods=["POST"]) @require_token -def approve_case_attributes_diff(inbredset_id: int, change_id: int, auth_token=None) -> Response: +def approve_case_attributes_diff( + inbredset_id: int, + change_id: int, auth_token=None +) -> tuple[Response, int]: """Approve the changes to the case attributes in the diff.""" try: required_access(auth_token, inbredset_id, - ("system:inbredset:edit-case-attribute")) - with database_connection(current_app.config["SQL_URI"]) as conn, \ - conn.cursor() as cursor: + ("system:inbredset:edit-case-attribute",)) + with (database_connection(current_app.config["SQL_URI"]) as conn, + conn.cursor() as cursor): + directory = (Path(current_app.config["LMDB_DATA_PATH"]) / + "case-attributes" / str(inbredset_id)) match apply_change(cursor, change_type=EditStatus.rejected, change_id=change_id, - directory=current_app.config["LMDB_DATA_PATH"]): + directory=directory): case True: return jsonify({ "diff-status": "rejected", "message": (f"Successfully approved # {change_id}") - }) + }), 201 case _: return jsonify({ "diff-status": "queued", "message": (f"Was not able to successfully approve # {change_id}") - }) + }), 200 except AuthorisationError as __auth_err: return jsonify({ "diff-status": "queued", @@ -300,7 +309,9 @@ def approve_case_attributes_diff(inbredset_id: int, change_id: int, auth_token=N @caseattr.route("/<int:inbredset_id>/reject/<int:change_id>", methods=["POST"]) @require_token -def reject_case_attributes_diff(inbredset_id: int, change_id: int, auth_token=None) -> Response: +def reject_case_attributes_diff( + inbredset_id: int, change_id: int, auth_token=None +) -> tuple[Response, int]: """Reject the changes to the case attributes in the diff.""" try: required_access(auth_token, @@ -309,20 +320,22 @@ def reject_case_attributes_diff(inbredset_id: int, change_id: int, auth_token=No "system:inbredset:apply-case-attribute-edit")) with database_connection(current_app.config["SQL_URI"]) as conn, \ conn.cursor() as cursor: + directory = (Path(current_app.config["LMDB_DATA_PATH"]) / + "case-attributes" / str(inbredset_id)) match apply_change(cursor, change_type=EditStatus.rejected, change_id=change_id, - directory=current_app.config["LMDB_DATA_PATH"]): + directory=directory): case True: return jsonify({ "diff-status": "rejected", "message": ("The changes to the case-attributes have been " "rejected.") - }) + }), 201 case _: return jsonify({ "diff-status": "queued", "message": ("Failed to reject changes") - }) + }), 200 except AuthorisationError as __auth_err: return jsonify({ "message": ("You don't have the right privileges to edit this resource.") @@ -331,7 +344,7 @@ def reject_case_attributes_diff(inbredset_id: int, change_id: int, auth_token=No @caseattr.route("/<int:inbredset_id>/diff/<int:change_id>/view", methods=["GET"]) @require_token -def view_diff(inbredset_id: int, change_id: int, auth_token=None) -> Response: +def view_diff(inbredset_id: int, change_id: int, auth_token=None) -> tuple[Response, int]: """View a diff.""" try: required_access( @@ -340,7 +353,7 @@ def view_diff(inbredset_id: int, change_id: int, auth_token=None) -> Response: conn.cursor(cursorclass=DictCursor) as cursor): return jsonify( view_change(cursor, change_id) - ) + ), 200 except AuthorisationError as __auth_err: return jsonify({ "message": ("You don't have the right privileges to view the diffs.") diff --git a/gn3/db/case_attributes.py b/gn3/db/case_attributes.py index b16bbdd..48afc0c 100644 --- a/gn3/db/case_attributes.py +++ b/gn3/db/case_attributes.py @@ -4,7 +4,6 @@ from typing import Optional from dataclasses import dataclass from enum import Enum, auto -import os import json import pickle import lmdb @@ -52,13 +51,6 @@ def queue_edit(cursor, directory: Path, edit: CaseAttributeEdit) -> Optional[int Returns: int: An id the particular case-attribute that was updated. - - Notes: - - Inserts the edit into the `caseattributes_audit` table with status set to - `EditStatus.review`. - - Uses LMDB to store review IDs under the key b"review" for the given - inbredset_id. - - The LMDB map_size is set to 8 MB. """ cursor.execute( "INSERT INTO " @@ -67,10 +59,8 @@ def queue_edit(cursor, directory: Path, edit: CaseAttributeEdit) -> Optional[int "ON DUPLICATE KEY UPDATE status=%s", (str(edit.status), edit.user_id, json.dumps(edit.changes), str(EditStatus.review),)) - directory = f"{directory}/case-attributes/{edit.inbredset_id}" - if not os.path.exists(directory): - os.makedirs(directory) - env = lmdb.open(directory, map_size=8_000_000) # 1 MB + directory.mkdir(parents=True, exist_ok=True) + env = lmdb.open(directory.as_posix(), map_size=8_000_000) # 1 MB with env.begin(write=True) as txn: review_ids = set() if reviews := txn.get(b"review"): @@ -143,16 +133,8 @@ def view_change(cursor, change_id: int) -> dict: Returns: dict: The deserialized JSON diff data as a dictionary if the - record exists and contains valid JSON; otherwise, an empty dictionary. - - Notes: - - The function assumes `change_id` is a valid integer - corresponding to a record in `caseattributes_audit`. - - The `json_diff_data` column is expected to contain a valid - JSON string or None. - - Uses parameterized queries to prevent SQL injection. - - The second column returned by `fetchone()` is - ignored (denoted by `_`). + record exists and contains valid JSON; otherwise, an + empty dictionary. Raises: json.JSONDecodeError: If the `json_diff_data` cannot be @@ -175,21 +157,18 @@ def view_change(cursor, change_id: int) -> dict: return {} -def get_changes(cursor, inbredset_id: int, directory: Path) -> dict: - """Retrieves case attribute changes for a given inbred set, - categorized by review status. +def get_changes(cursor, directory: Path) -> dict: + """Retrieves case attribute changes for given lmdb data in + directory categorized by review status. - Fetches change IDs from an LMDB database for the specified - `inbredset_id`, categorized as review, approved, or - rejected. Queries the `caseattributes_audit` table to retrieve - details for these changes using the internal + Fetches change IDs from an LMDB database, categorized as review, + approved, or rejected. Queries the `caseattributes_audit` table to + retrieve details for these changes using the internal `__fetch_case_attrs_changes__` function. Returns a dictionary with change details grouped by status. Args: - cursor: A MySQLdb cursor for executing SQL queries. - - inbredset_id (int): The ID of the inbred set to filter - changes. - directory (Path): The base directory path for the LMDB database. @@ -201,20 +180,6 @@ def get_changes(cursor, inbredset_id: int, directory: Path) -> dict: 'time_stamp'). Empty dictionaries are returned for categories with no changes. - Notes: - - Creates an LMDB environment in a subdirectory - `case-attributes/{inbredset_id}` if it doesn't exist, with a - map size of 8 MB. - - Uses LMDB to store sets of change IDs under keys 'review', - 'approved', and 'rejected'. - - The `__fetch_case_attrs_changes__` function is called for - each non-empty set of change IDs to retrieve details from - the `caseattributes_audit` table. - - If no change IDs exist for a category, an empty dictionary - is returned for that category. - - Assumes `inbredset_id` is a valid integer and the LMDB - database is properly formatted. - Raises: json.JSONDecodeError: If any `json_diff_data` in the audit table cannot be deserialized by @@ -223,11 +188,10 @@ def get_changes(cursor, inbredset_id: int, directory: Path) -> dict: cannot be deserialized. """ - directory = f"{directory}/case-attributes/{inbredset_id}" - if not os.path.exists(directory): - os.makedirs(directory) + directory.mkdir(parents=True, exist_ok=True) review_ids, approved_ids, rejected_ids = set(), set(), set() - env = lmdb.open(directory, map_size=8_000_000) # 1 MB + directory.mkdir(parents=True, exist_ok=True) + env = lmdb.open(directory.as_posix(), map_size=8_000_000) # 1 MB with env.begin(write=False) as txn: if reviews := txn.get(b"review"): review_ids = pickle.loads(reviews) @@ -313,7 +277,8 @@ def apply_change(cursor, change_type: EditStatus, change_id: int, directory: Pat """ review_ids, approved_ids, rejected_ids = set(), set(), set() - env = lmdb.open(directory, map_size=8_000_000) # 1 MB + directory.mkdir(parents=True, exist_ok=True) + env = lmdb.open(directory.as_posix(), map_size=8_000_000) # 1 MB with env.begin(write=True) as txn: if reviews := txn.get(b"review"): review_ids = pickle.loads(reviews) diff --git a/gn3/db/sample_data.py b/gn3/db/sample_data.py index 446d18d..4e01a3a 100644 --- a/gn3/db/sample_data.py +++ b/gn3/db/sample_data.py @@ -59,7 +59,7 @@ def __extract_actions( return result def get_mrna_sample_data( - conn: Any, probeset_id: int, dataset_name: str, probeset_name: str = None + conn: Any, probeset_id: int, dataset_name: str, probeset_name: str = None # type: ignore ) -> Dict: """Fetch a mRNA Assay (ProbeSet in the DB) trait's sample data and return it as a dict""" with conn.cursor() as cursor: @@ -130,7 +130,7 @@ WHERE ps.Id = %s AND psf.Name= %s""", (probeset_id, dataset_name)) return "\n".join(trait_csv) def get_pheno_sample_data( - conn: Any, trait_name: int, phenotype_id: int, group_id: int = None + conn: Any, trait_name: int, phenotype_id: int, group_id: int = None # type: ignore ) -> Dict: """Fetch a phenotype (Publish in the DB) trait's sample data and return it as a dict""" with conn.cursor() as cursor: diff --git a/tests/unit/db/test_case_attributes.py b/tests/unit/db/test_case_attributes.py index d9da253..9b0378f 100644 --- a/tests/unit/db/test_case_attributes.py +++ b/tests/unit/db/test_case_attributes.py @@ -22,7 +22,7 @@ def test_queue_edit(mocker: MockFixture) -> None: mock_conn = mocker.MagicMock() with mock_conn.cursor() as cursor: type(cursor).lastrowid = 28 - tmpdir = os.environ.get("TMPDIR", tempfile.gettempdir()) + tmpdir = Path(os.environ.get("TMPDIR", tempfile.gettempdir())) caseattr_id = queue_edit( cursor, directory=tmpdir, |
