Skip to content

Commit

Permalink
Merge branch 'devel' into render_product_metrics
Browse files Browse the repository at this point in the history
  • Loading branch information
nerdstrike authored Jun 14, 2024
2 parents 579138b + 0b225e7 commit d8b755d
Show file tree
Hide file tree
Showing 17 changed files with 695 additions and 74 deletions.
12 changes: 11 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,16 @@ and this project adheres to [Semantic Versioning](http://semver.org/).

## [Unreleased]

## [2.2.0] - 2024-06-11

### Added

* New endpoint added to support potential email notifications when QC states are finalised. `/products/qc?weeks={n}&seq_level={bool}&final={bool}`. It returns recent QC events

### Fixed

* Warehouse schema updated to match breaking change in pac_bio_product_metrics table

## [2.1.0] - 2024-04-15

### Changed
Expand Down Expand Up @@ -77,7 +87,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
* The client side JavaScript dependency, element-plus, is pinned
to version 2.4.4. In mid-January 2024 this was the highest version
that worked with our code. The version expression we had "^2.3.7"
allowed for fetching the latest available version of this library.
allowed for fetching the latest available version of this library.

## [1.5.0]

Expand Down
2 changes: 1 addition & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "npg-longue-vue",
"version": "2.1.0",
"version": "2.2.0",
"description": "UI for LangQC",
"author": "Kieron Taylor <[email protected]>",
"license": "GPL-3.0-or-later",
Expand Down
2 changes: 1 addition & 1 deletion lang_qc/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "2.1.0"
__version__ = "2.2.0"
58 changes: 57 additions & 1 deletion lang_qc/db/helper/qc.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
# this program. If not, see <http://www.gnu.org/licenses/>.

from collections import defaultdict
from datetime import datetime
from datetime import date, datetime, timedelta

from sqlalchemy import and_, func, select
from sqlalchemy.exc import NoResultFound
Expand Down Expand Up @@ -105,6 +105,62 @@ def get_qc_states_by_id_product_list(
return dict(response)


def get_qc_states(
session: Session,
num_weeks: int,
sequencing_outcomes_only: bool = False,
final_only: bool = False,
) -> dict[ChecksumSHA256, list[QcState]]:
"""
Returns a dictionary where keys are the product IDs, and the values are
lists of QcState records of any type for the same product.
The num_weeks argument limits the look-back time window.
If only sequencing type QC states are required, an optional
argument, sequencing_outcomes_only, should be set to True.
In this case it is guaranteed that the list of QcState objects
has only one member.
If only final QC states are required, an optional argument final_only
should be set to True.
"""

if num_weeks < 1:
raise ValueError("num_weeks should be a positive number")

query = (
select(QcStateDb)
.join(QcStateDb.seq_product)
.join(QcType)
.join(QcStateDict)
.join(User)
.where(QcStateDb.date_updated > date.today() - timedelta(weeks=num_weeks))
.options(
selectinload(QcStateDb.seq_product),
selectinload(QcStateDb.qc_type),
selectinload(QcStateDb.user),
selectinload(QcStateDb.qc_state_dict),
)
)
if sequencing_outcomes_only is True:
query = query.where(QcType.qc_type == SEQUENCING_QC_TYPE)
if final_only is True:
query = query.where(QcStateDb.is_preliminary == 0)

qc_states_dict = dict()
for qc_state in [
QcState.from_orm(row) for row in session.execute(query).scalars().all()
]:
id = qc_state.id_product
if id in qc_states_dict:
qc_states_dict[id].append(qc_state)
else:
qc_states_dict[id] = [qc_state]

return qc_states_dict


def product_has_qc_state(
session: Session, id_product: ChecksumSHA256, qc_type: str = None
) -> bool:
Expand Down
36 changes: 33 additions & 3 deletions lang_qc/db/mlwh_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,30 @@
from sqlalchemy.dialects.mysql import SMALLINT as mysqlSMALLINT
from sqlalchemy.dialects.mysql import TINYINT as mysqlTINYINT
from sqlalchemy.dialects.mysql import VARCHAR as mysqlVARCHAR
from sqlalchemy.orm import declarative_base, relationship
from sqlalchemy.orm import DeclarativeBase, relationship

Base = declarative_base()

class Base(DeclarativeBase):
"""
A base class for declarative class definitions for the ml warehouse database.
"""

def _get_row_description(self, fields: list[str]) -> str:
"""
Returns a printable representation of the database table row. Interprets
a list of strings given as the `fields` argument as a list of column
names. Combines the name of the class, names of the given columns
and respective values into a row description. The columns for which
the row has a NULL value are omitted from the description.
"""

pairs = []
for name in fields:
value = self.__getattribute__(name)
if value is not None:
pairs.append(f"{name}={value}")
description = ", ".join(pairs)
return f"{self.__module__}.{self.__class__.__name__}: {description}"


class Sample(Base):
Expand Down Expand Up @@ -538,7 +559,16 @@ class PacBioRunWellMetrics(Base):
"PacBioProductMetrics", back_populates="pac_bio_run_well_metrics"
)

def get_experiment_info(self):
"""Custom or customised methods are added below"""

def __repr__(self):
"""Returns a printable representation of the database row"""

return self._get_row_description(
["pac_bio_run_name", "well_label", "plate_number", "id_pac_bio_product"]
)

def get_experiment_info(self) -> list[PacBioRun]:
"""Returns a list of PacBioRun mlwh database rows.
Returns LIMS information about the PacBio experiment
Expand Down
33 changes: 32 additions & 1 deletion lang_qc/endpoints/pacbio_well.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,18 @@
from lang_qc.db.qc_connection import get_qc_db
from lang_qc.db.qc_schema import User
from lang_qc.models.pacbio.qc_data import QCPoolMetrics
from lang_qc.models.pacbio.well import PacBioPagedWells, PacBioWellFull
from lang_qc.models.pacbio.well import (
PacBioPagedWells,
PacBioWellFull,
PacBioWellLibraries,
)
from lang_qc.models.qc_flow_status import QcFlowStatusEnum
from lang_qc.models.qc_state import QcState, QcStateBasic
from lang_qc.util.auth import check_user
from lang_qc.util.errors import (
InconsistentInputError,
InvalidDictValueError,
MissingLimsDataError,
RunNotFoundError,
)
from lang_qc.util.type_checksum import ChecksumSHA256, PacBioWellSHA256
Expand Down Expand Up @@ -164,6 +169,32 @@ def get_wells_in_run(
return response


@router.get(
"/wells/{id_product}/libraries",
summary="Get well summary and LIMS data for all libraries",
responses={
status.HTTP_404_NOT_FOUND: {"description": "Well product does not exist"},
status.HTTP_422_UNPROCESSABLE_ENTITY: {"description": "Invalid product ID"},
status.HTTP_409_CONFLICT: {"description": "Missing or incomplete LIMS data"},
},
response_model=PacBioWellLibraries,
)
def get_well_lims_info(
id_product: ChecksumSHA256,
mlwhdb_session: Session = Depends(get_mlwh_db),
) -> PacBioWellLibraries:

db_well = _find_well_product_or_error(id_product, mlwhdb_session)
well_libraries: PacBioWellLibraries
try:
well_libraries = PacBioWellLibraries(db_well=db_well)
except MissingLimsDataError as err:
# 409 - Request conflicts with the current state of the server.
raise HTTPException(409, detail=str(err))

return well_libraries


@router.get(
"/products/{id_product}/seq_level",
summary="Get full sequencing QC metrics and state for a product",
Expand Down
44 changes: 42 additions & 2 deletions lang_qc/endpoints/product.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,19 @@
# You should have received a copy of the GNU General Public License along with
# this program. If not, see <http://www.gnu.org/licenses/>.

from fastapi import APIRouter, Depends
from typing import Annotated

from fastapi import APIRouter, Depends, Query
from sqlalchemy.orm import Session
from starlette import status

from lang_qc.db.helper.qc import get_qc_states_by_id_product_list
from lang_qc.db.helper.qc import get_qc_states, get_qc_states_by_id_product_list
from lang_qc.db.qc_connection import get_qc_db
from lang_qc.models.qc_state import QcState
from lang_qc.util.type_checksum import ChecksumSHA256

RECENTLY_QCED_NUM_WEEKS = 4

router = APIRouter(
prefix="/products",
tags=["product"],
Expand Down Expand Up @@ -62,3 +66,39 @@ def bulk_qc_fetch(
):

return get_qc_states_by_id_product_list(session=qcdb_session, ids=request_body)


@router.get(
"/qc",
summary="Returns a dictionary of QC states",
description="""
The response is a dictionary of lists of QcState models hashed on product IDs.
Multiple QC states for the same product might be returned if the query is not
constrained to a single QC type.
Query parameters constrain the semantics of the response.
`weeks` - number of weeks to look back, defaults to four.
`seq_level` - a boolean option. If `True`, only `sequencing` type QC states
are returned. If `False` (the default), all types of QC states are
returned.
`final` - a boolean option. If `True`, only final QC states are returned.
If `False` (the default), both final and preliminary QC states are
returned.
""",
responses={
status.HTTP_422_UNPROCESSABLE_ENTITY: {"description": "Invalid number of weeks"}
},
response_model=dict[ChecksumSHA256, list[QcState]],
)
def qc_fetch(
weeks: Annotated[int, Query(gt=0)] = RECENTLY_QCED_NUM_WEEKS,
seq_level: bool = False,
final: bool = False,
qcdb_session: Session = Depends(get_qc_db),
) -> dict[ChecksumSHA256, list[QcState]]:
return get_qc_states(
session=qcdb_session,
num_weeks=weeks,
sequencing_outcomes_only=seq_level,
final_only=final,
)
Loading

0 comments on commit d8b755d

Please sign in to comment.