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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@ and this project adheres to [Semantic Versioning](http://semver.org/).

## [Unreleased]

## [1.2.0] 2026-09-16

### Added

* `validate_studies` and `get_sample_id_for_name` functions are added to
`npgmlwarehouse.db.product`. Both are needed for extending phase2 Ultimagen
pipeline to walkup runs.

## [1.1.1] - 2026-05-20

### Fixed
Expand Down
88 changes: 86 additions & 2 deletions src/npgmlwarehouse/db/product.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,16 @@
# 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 sqlalchemy import select
from sqlalchemy import or_, select
from sqlalchemy.dialects.mysql import insert
from sqlalchemy.orm import Session

from npgmlwarehouse.db.schema import SeqProductIrodsLocations, UseqProductMetrics
from npgmlwarehouse.db.schema import (
Sample,
SeqProductIrodsLocations,
Study,
UseqProductMetrics,
)


def get_ultimagen_target_product_records(session: Session, id_run: int):
Expand Down Expand Up @@ -47,6 +52,85 @@ def get_ultimagen_target_product_records(session: Session, id_run: int):
return records.all()


def validate_studies(session: Session, study_ids: list[str]):
"""
Validates a list of string study IDs.

Validates a list of study IDs by checking whether the `study` table
record is available for each of the IDs. The input least can contain
duplicate values. An empty input list will cause an error.

Errors if any of the given study IDs
* is not a string,
* is invalid, the error message lists all invalid IDs,
* maps to multiple database rows.

Args:
session (Session):
Database session.
study_ids (str):
Study IDs to validate.
Returns:
True if all sudies have been validated.
"""

if len(study_ids) == 0:
raise ValueError("A non-empty list of string study IDs is required")
if len([id for id in study_ids if isinstance(id, str)]) < len(study_ids):
raise TypeError("All study IDs should be strings")

unique_ids = set(study_ids)

q = select(Study.id_study_lims).where(Study.id_study_lims.in_(unique_ids))
available = session.scalars(q).all()
missing = [study_id for study_id in unique_ids if study_id not in available]
if len(missing):
raise ValueError(
"Invalid stud{end}: {list_of}".format(
end="y" if len(missing) == 1 else "ies",
list_of=", ".join(sorted(missing)),
)
)
if len(available) > len(unique_ids):
raise Exception(
(
"Duplicate MLWH rows for some IDs in "
f"{', '.join(sorted(list(unique_ids)))}"
)
)

return True


def get_sample_id_for_name(session: Session, sample_name: str) -> str | None:
"""
Retrieves sample ID for the sample name given as the argument.

Searches the `sample` table for a record with the given name. Examines
values in two columns - `name` and `supplier_name`.

Errors if multiple records are found. Returns sample's ID if a single
record is found or `None` if no record is found.

Args:
session (Session):
Database session.
name (name):
Sample name.
Returns:
String sample ID if found or None
"""
if not isinstance(sample_name, str):
raise TypeError("sample_name argument should be a string")
if not sample_name:
raise ValueError("sample_name argument should be a non-empty string")

q = select(Sample.id_sample_lims).where(
or_(Sample.name == sample_name, Sample.supplier_name == sample_name)
)
return session.scalars(q).one_or_none()


def create_upload_irods_location_records(
session: Session,
product_data: dict[str, dict],
Expand Down
3 changes: 3 additions & 0 deletions tests/data/db_fixtures/000-Sample.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
id_sample_lims: 10000
id_sample_tmp: 1
last_updated: 2012-08-03 10:07:02
name: 'Burkholderia 1'
organism: Burkholderia pseudomallei
recorded_at: 2014-11-18 13:06:19
reference_genome: ' '
Expand All @@ -27,6 +28,8 @@
id_sample_lims: 10001
id_sample_tmp: 2
last_updated: 2012-08-03 10:07:02
name: 'Burkholderia 22'
supplier_name: 'Burkholderia 2'
organism: Burkholderia pseudomallei
recorded_at: 2014-11-18 13:06:19
reference_genome: ' '
Expand Down
104 changes: 103 additions & 1 deletion tests/data/db_fixtures/000-Study.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
- abbreviation: 619STDY
abstract: For further information on this study please see
accession_number: ERP000251
accession_number: 251
aligned: 1
array_express_accession_number: ~
contains_human_dna: 0
Expand Down Expand Up @@ -33,3 +33,105 @@
study_type: Whole Genome Sequencing
study_visibility: Hold
uuid_study_lims: 1231de50-6ffb-11e4-b9db-68b59977951c
- abbreviation: 622STDY
abstract: For further information on this study please see
accession_number: 34
aligned: 1
array_express_accession_number: ~
contains_human_dna: 0
contaminated_human_dna: 0
created: 2010-06-25 11:19:52
data_access_group: ~
data_release_delay_period: ~
data_release_delay_reason: ~
data_release_sort_of_study: genomic sequencing
data_release_strategy: open
data_release_timing: standard
deleted_at: ~
description: 'For further information on this study please'
ega_dac_accession_number: ~
ega_policy_accession_number: ~
ena_project_id: 0
ethically_approved: ~
id_lims: SQSCP
id_study_lims: 622
id_study_tmp: 197
last_updated: 2012-11-26 14:15:32
name: Burkholderia pseudomallei
recorded_at: 2014-11-19 14:48:06
reference_genome: ' '
remove_x_and_autosomes: 0
separate_y_chromosome_data: 0
state: active
study_title: Burkholderia pseudomallei
study_type: Whole Genome Sequencing
study_visibility: Hold
uuid_study_lims: 1231de50-6ffb-11e4-b9db-68b59977951b
- abbreviation: 623STDY
abstract: For further information on this study please see
accession_number: 34
aligned: 1
array_express_accession_number: ~
contains_human_dna: 0
contaminated_human_dna: 0
created: 2010-06-25 11:19:52
data_access_group: ~
data_release_delay_period: ~
data_release_delay_reason: ~
data_release_sort_of_study: genomic sequencing
data_release_strategy: open
data_release_timing: standard
deleted_at: ~
description: 'For further information on this study please'
ega_dac_accession_number: ~
ega_policy_accession_number: ~
ena_project_id: 0
ethically_approved: ~
id_lims: SQSCP
id_study_lims: 623
id_study_tmp: 198
last_updated: 2012-11-26 14:15:32
name: Burkholderia pseudomallei
recorded_at: 2014-11-19 14:48:06
reference_genome: ' '
remove_x_and_autosomes: 0
separate_y_chromosome_data: 0
state: active
study_title: Burkholderia pseudomallei
study_type: Whole Genome Sequencing
study_visibility: Hold
uuid_study_lims: 1231de50-6ffb-11e4-b9db-68b59977951d
- abbreviation: 623STDY
abstract: For further information on this study please see
accession_number: 34
aligned: 1
array_express_accession_number: ~
contains_human_dna: 0
contaminated_human_dna: 0
created: 2010-06-25 11:19:52
data_access_group: ~
data_release_delay_period: ~
data_release_delay_reason: ~
data_release_sort_of_study: genomic sequencing
data_release_strategy: open
data_release_timing: standard
deleted_at: ~
description: 'For further information on this study please'
ega_dac_accession_number: ~
ega_policy_accession_number: ~
ena_project_id: 0
ethically_approved: ~
id_lims: Sapio
id_study_lims: 623
id_study_tmp: 199
last_updated: 2012-11-26 14:15:32
name: Burkholderia pseudomallei
recorded_at: 2014-11-19 14:48:06
reference_genome: ' '
remove_x_and_autosomes: 0
separate_y_chromosome_data: 0
state: active
study_title: Burkholderia pseudomallei
study_type: Whole Genome Sequencing
study_visibility: Hold
uuid_study_lims: 1231de50-6ffb-11e4-b9db-68b59977951k
49 changes: 49 additions & 0 deletions tests/test_product.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
from time import sleep

from pytest import mark as m
from pytest import raises
from sqlalchemy import select

from npgmlwarehouse.db.product import (
create_upload_irods_location_records,
get_sample_id_for_name,
get_ultimagen_target_product_records,
validate_studies,
)
from npgmlwarehouse.db.schema import SeqProductIrodsLocations

Expand Down Expand Up @@ -385,3 +388,49 @@ def test_create_upload_irods_location_records_duplicate_unique_key_part_input_up
record = records.pop()
assert record.irods_data_relative_path == None
assert record.irods_secondary_data_relative_path == None


@m.describe("Test Sample Retrieval")
class TestSampleRetrieval(object):
def test_get_sample_id(self, testdb):

with raises(TypeError):
get_sample_id_for_name(testdb)
with raises(TypeError, match=r"sample_name argument should be a string"):
get_sample_id_for_name(testdb, 45)
with raises(TypeError, match=r"sample_name argument should be a string"):
get_sample_id_for_name(testdb, None)
with raises(
ValueError, match=r"sample_name argument should be a non-empty string"
):
get_sample_id_for_name(testdb, "")

assert get_sample_id_for_name(testdb, "Burkholderia 1") == "10000"
assert get_sample_id_for_name(testdb, "Burkholderia 2") == "10001"
assert get_sample_id_for_name(testdb, "Burkholderia 25") is None


@m.describe("Test Study Validation")
class TestStudyValidation(object):
def test_validate_studies(self, testdb):

with raises(TypeError):
assert validate_studies(testdb)
with raises(
ValueError, match=r"A non-empty list of string study IDs is required"
):
assert validate_studies(testdb, [])
with raises(TypeError, match=r"All study IDs should be strings"):
assert validate_studies(testdb, ["1234", 34])
with raises(Exception, match=r"Duplicate MLWH rows for some IDs in 619, 623"):
assert validate_studies(testdb, ["623", "619", "623"])

assert validate_studies(testdb, ["619"]) is True
assert validate_studies(testdb, ["619", "622"]) is True

with raises(ValueError, match=r"Invalid study: 620"):
assert validate_studies(testdb, ["620"])
with raises(ValueError, match=r"Invalid studies: 620, 621"):
assert validate_studies(testdb, ["621", "620"])
with raises(ValueError, match=r"Invalid study: 620"):
assert validate_studies(testdb, ["619", "620"])