diff --git a/pyproject.toml b/pyproject.toml index 874b1b2..945ec5c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,54 +1,40 @@ [project] name = "ga4gh.cat_vrs" authors = [ - {name = "Larry Babb", email = "lbabb@broadinstitute.org"}, - {name = "Alex Wagner", email = "alex.wagner@nationwidechildrens.org"}, - {name = "Daniel Puthawala", email = "daniel.puthawala@nationwidechildrens.org"}, + { name = "Larry Babb", email = "lbabb@broadinstitute.org" }, + { name = "Alex Wagner", email = "alex.wagner@nationwidechildrens.org" }, + { name = "Daniel Puthawala", email = "daniel.puthawala@nationwidechildrens.org" }, ] readme = "README.md" description = "GA4GH Categorical Variation Representation (Cat-VRS) reference implementation" -license = {file = "LICENSE"} +license = { file = "LICENSE" } classifiers = [ - "Development Status :: 3 - Alpha", - "Intended Audience :: Developers", - "Intended Audience :: Healthcare Industry", - "Intended Audience :: Science/Research", - "License :: OSI Approved :: Apache Software License", - "Operating System :: OS Independent", - "Programming Language :: Python", - "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.10", - "Programming Language :: Python :: 3.11", - "Programming Language :: Python :: 3.12", - "Topic :: Scientific/Engineering :: Bio-Informatics", - "Topic :: Scientific/Engineering :: Medical Science Apps.", -] -keywords = [ - "bioinformatics", - "ga4gh", - "genomics", - "variation" + "Development Status :: 3 - Alpha", + "Intended Audience :: Developers", + "Intended Audience :: Healthcare Industry", + "Intended Audience :: Science/Research", + "License :: OSI Approved :: Apache Software License", + "Operating System :: OS Independent", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Topic :: Scientific/Engineering :: Bio-Informatics", + "Topic :: Scientific/Engineering :: Medical Science Apps.", ] +keywords = ["bioinformatics", "ga4gh", "genomics", "variation"] requires-python = ">=3.10" dynamic = ["version"] dependencies = [ - "ga4gh.vrs~=2.4.0a1", - "pydantic>=2.0,<3.0", + "ga4gh.vrs @ git+https://github.com/ga4gh/vrs-python.git@daceb0b6cd773f8c4c5b6dff560fb7f4a5ff7b34", + "pydantic>=2.0,<3.0", ] [project.optional-dependencies] -dev = [ - "pre-commit", - "ruff==0.4.9", -] -tests = [ - "pytest", - "pytest-cov", - "pyyaml", -] -notebooks = [ - "jupyter", -] +dev = ["pre-commit", "ruff==0.4.9"] +tests = ["pytest", "pytest-cov", "pyyaml"] +notebooks = ["jupyter"] [project.urls] Homepage = "https://github.com/ga4gh/cat-vrs-python" @@ -63,53 +49,52 @@ build-backend = "setuptools.build_meta" [tool.ruff] src = ["src"] -exclude = [ - "submodules" -] +exclude = ["submodules"] [tool.ruff.lint] select = [ - "F", # https://docs.astral.sh/ruff/rules/#pyflakes-f - "E", "W", # https://docs.astral.sh/ruff/rules/#pycodestyle-e-w - "I", # https://docs.astral.sh/ruff/rules/#isort-i - "N", # https://docs.astral.sh/ruff/rules/#pep8-naming-n - "D", # https://docs.astral.sh/ruff/rules/#pydocstyle-d - "UP", # https://docs.astral.sh/ruff/rules/#pyupgrade-up - "ANN", # https://docs.astral.sh/ruff/rules/#flake8-annotations-ann - "ASYNC", # https://docs.astral.sh/ruff/rules/#flake8-async-async - "S", # https://docs.astral.sh/ruff/rules/#flake8-bandit-s - "B", # https://docs.astral.sh/ruff/rules/#flake8-bugbear-b - "A", # https://docs.astral.sh/ruff/rules/#flake8-builtins-a - "C4", # https://docs.astral.sh/ruff/rules/#flake8-comprehensions-c4 - "DTZ", # https://docs.astral.sh/ruff/rules/#flake8-datetimez-dtz - "T10", # https://docs.astral.sh/ruff/rules/#flake8-datetimez-dtz - "EM", # https://docs.astral.sh/ruff/rules/#flake8-errmsg-em - "G", # https://docs.astral.sh/ruff/rules/#flake8-logging-format-g - "PIE", # https://docs.astral.sh/ruff/rules/#flake8-pie-pie - "T20", # https://docs.astral.sh/ruff/rules/#flake8-print-t20 - "PT", # https://docs.astral.sh/ruff/rules/#flake8-pytest-style-pt - "Q", # https://docs.astral.sh/ruff/rules/#flake8-quotes-q - "RSE", # https://docs.astral.sh/ruff/rules/#flake8-raise-rse - "RET", # https://docs.astral.sh/ruff/rules/#flake8-return-ret - "SIM", # https://docs.astral.sh/ruff/rules/#flake8-simplify-sim - "PTH", # https://docs.astral.sh/ruff/rules/#flake8-use-pathlib-pth - "PGH", # https://docs.astral.sh/ruff/rules/#pygrep-hooks-pgh - "RUF", # https://docs.astral.sh/ruff/rules/#ruff-specific-rules-ruf + "F", # https://docs.astral.sh/ruff/rules/#pyflakes-f + "E", + "W", # https://docs.astral.sh/ruff/rules/#pycodestyle-e-w + "I", # https://docs.astral.sh/ruff/rules/#isort-i + "N", # https://docs.astral.sh/ruff/rules/#pep8-naming-n + "D", # https://docs.astral.sh/ruff/rules/#pydocstyle-d + "UP", # https://docs.astral.sh/ruff/rules/#pyupgrade-up + "ANN", # https://docs.astral.sh/ruff/rules/#flake8-annotations-ann + "ASYNC", # https://docs.astral.sh/ruff/rules/#flake8-async-async + "S", # https://docs.astral.sh/ruff/rules/#flake8-bandit-s + "B", # https://docs.astral.sh/ruff/rules/#flake8-bugbear-b + "A", # https://docs.astral.sh/ruff/rules/#flake8-builtins-a + "C4", # https://docs.astral.sh/ruff/rules/#flake8-comprehensions-c4 + "DTZ", # https://docs.astral.sh/ruff/rules/#flake8-datetimez-dtz + "T10", # https://docs.astral.sh/ruff/rules/#flake8-datetimez-dtz + "EM", # https://docs.astral.sh/ruff/rules/#flake8-errmsg-em + "G", # https://docs.astral.sh/ruff/rules/#flake8-logging-format-g + "PIE", # https://docs.astral.sh/ruff/rules/#flake8-pie-pie + "T20", # https://docs.astral.sh/ruff/rules/#flake8-print-t20 + "PT", # https://docs.astral.sh/ruff/rules/#flake8-pytest-style-pt + "Q", # https://docs.astral.sh/ruff/rules/#flake8-quotes-q + "RSE", # https://docs.astral.sh/ruff/rules/#flake8-raise-rse + "RET", # https://docs.astral.sh/ruff/rules/#flake8-return-ret + "SIM", # https://docs.astral.sh/ruff/rules/#flake8-simplify-sim + "PTH", # https://docs.astral.sh/ruff/rules/#flake8-use-pathlib-pth + "PGH", # https://docs.astral.sh/ruff/rules/#pygrep-hooks-pgh + "RUF", # https://docs.astral.sh/ruff/rules/#ruff-specific-rules-ruf ] fixable = [ - "I", - "F401", - "D", - "UP", - "ANN", - "B", - "C4", - "G", - "PIE", - "PT", - "RSE", - "SIM", - "RUF" + "I", + "F401", + "D", + "UP", + "ANN", + "B", + "C4", + "G", + "PIE", + "PT", + "RSE", + "SIM", + "RUF", ] # ANN101 - missing-type-self # ANN003 - missing-type-kwargs @@ -128,11 +113,21 @@ fixable = [ # S321 - suspicious-ftp-lib-usage # *ignored for compatibility with formatter ignore = [ - "ANN101", "ANN003", - "D203", "D205", "D206", "D213", "D300", "D400", "D415", - "E111", "E114", "E117", "E501", - "W191", - "S321", + "ANN101", + "ANN003", + "D203", + "D205", + "D206", + "D213", + "D300", + "D400", + "D415", + "E111", + "E114", + "E117", + "E501", + "W191", + "S321", ] [tool.ruff.lint.per-file-ignores] diff --git a/src/ga4gh/cat_vrs/__init__.py b/src/ga4gh/cat_vrs/__init__.py index 475d909..380b27e 100644 --- a/src/ga4gh/cat_vrs/__init__.py +++ b/src/ga4gh/cat_vrs/__init__.py @@ -1,21 +1,23 @@ """Package for Cat-VRS Python implementation""" -from importlib.metadata import PackageNotFoundError, version +from importlib.metadata import PackageNotFoundError +from importlib.metadata import version as package_version from . import models, recipes +from .metadata import CatVRSMetadataMixin +from .version import CATVRS_VERSION try: - __version__ = version(__name__) + __version__ = package_version(__name__) except PackageNotFoundError: # pragma: nocover __version__ = "unknown" finally: - del version, PackageNotFoundError + del package_version, PackageNotFoundError -CATVRS_VERSION = "1.1.0-snapshot.2026-02.3" - __all__ = [ "CATVRS_VERSION", + "CatVRSMetadataMixin", "__version__", "models", "recipes", diff --git a/src/ga4gh/cat_vrs/metadata.py b/src/ga4gh/cat_vrs/metadata.py new file mode 100644 index 0000000..7bbc397 --- /dev/null +++ b/src/ga4gh/cat_vrs/metadata.py @@ -0,0 +1,11 @@ +"""Provide metadata mixins for Cat-VRS models.""" + +from ga4gh.cat_vrs.version import CATVRS_VERSION +from ga4gh.core.metadata import GKSMetadataMixin + + +class CatVRSMetadataMixin(GKSMetadataMixin): + """Provide metadata for a concrete Cat-VRS model.""" + + _product_name = "cat-vrs" + _product_version = CATVRS_VERSION diff --git a/src/ga4gh/cat_vrs/models.py b/src/ga4gh/cat_vrs/models.py index 50060d7..03e6ac8 100644 --- a/src/ga4gh/cat_vrs/models.py +++ b/src/ga4gh/cat_vrs/models.py @@ -4,10 +4,12 @@ the GA4GH website for more information. """ -from typing import Literal +from typing import ClassVar, Literal from pydantic import Field, RootModel +from ga4gh.cat_vrs.metadata import CatVRSMetadataMixin +from ga4gh.core.metadata import Maturity from ga4gh.core.models import ( BaseModelForbidExtra, ConceptMapping, @@ -18,11 +20,13 @@ from ga4gh.vrs.models import Allele, CopyChange, Range, SequenceLocation, Variation -class DefiningAlleleConstraint(BaseModelForbidExtra): +class DefiningAlleleConstraint(CatVRSMetadataMixin, BaseModelForbidExtra): """The defining allele and its associated relationships that are congruent with member variants. """ + _maturity: ClassVar[Maturity] = Maturity.TRIAL_USE + type: Literal["DefiningAlleleConstraint"] = Field( default="DefiningAlleleConstraint", description="MUST be 'DefiningAlleleConstraint'", @@ -34,11 +38,13 @@ class DefiningAlleleConstraint(BaseModelForbidExtra): ) -class DefiningLocationConstraint(BaseModelForbidExtra): +class DefiningLocationConstraint(CatVRSMetadataMixin, BaseModelForbidExtra): """The defining location and its associated relationships that are congruent with member locations. """ + _maturity: ClassVar[Maturity] = Maturity.TRIAL_USE + type: Literal["DefiningLocationConstraint"] = Field( default="DefiningLocationConstraint", description="MUST be 'DefiningLocationConstraint'", @@ -54,11 +60,13 @@ class DefiningLocationConstraint(BaseModelForbidExtra): ) -class CopyCountConstraint(BaseModelForbidExtra): +class CopyCountConstraint(CatVRSMetadataMixin, BaseModelForbidExtra): """The exact or range of copies that members of this categorical variant must satisfy. """ + _maturity: ClassVar[Maturity] = Maturity.TRIAL_USE + type: Literal["CopyCountConstraint"] = Field( default="CopyCountConstraint", description="MUST be 'CopyCountConstraint'" ) @@ -68,11 +76,13 @@ class CopyCountConstraint(BaseModelForbidExtra): ) -class CopyChangeConstraint(BaseModelForbidExtra): +class CopyChangeConstraint(CatVRSMetadataMixin, BaseModelForbidExtra): """The relative assessment of the change in copies that members of this categorical variant satisfy. """ + _maturity: ClassVar[Maturity] = Maturity.DRAFT + type: Literal["CopyChangeConstraint"] = Field( default="CopyChangeConstraint", description="MUST be 'CopyChangeConstraint'" ) @@ -82,9 +92,11 @@ class CopyChangeConstraint(BaseModelForbidExtra): ) -class FeatureContextConstraint(BaseModelForbidExtra): +class FeatureContextConstraint(CatVRSMetadataMixin, BaseModelForbidExtra): """The feature that members of this categorical variant are associated with.""" + _maturity: ClassVar[Maturity] = Maturity.DRAFT + type: Literal["FeatureContextConstraint"] = Field( default="FeatureContextConstraint", description="MUST be 'FeatureContextConstraint'", @@ -92,9 +104,11 @@ class FeatureContextConstraint(BaseModelForbidExtra): featureContext: MappableConcept = Field(..., description="A feature identifier.") -class FunctionConstraint(BaseModelForbidExtra): +class FunctionConstraint(CatVRSMetadataMixin, BaseModelForbidExtra): """A classification of the protein functional consequence that characterizes members of this categorical variant.""" + _maturity: ClassVar[Maturity] = Maturity.DRAFT + type: Literal["FunctionConstraint"] = Field( default="FunctionConstraint", description='MUST be "FunctionConstraint"', @@ -108,9 +122,11 @@ class FunctionConstraint(BaseModelForbidExtra): ) -class Constraint(RootModel): +class Constraint(CatVRSMetadataMixin, RootModel): """Constraints are used to construct an intensional semantics of categorical variant types.""" + _maturity: ClassVar[Maturity] = Maturity.TRIAL_USE + root: ( DefiningAlleleConstraint | DefiningLocationConstraint @@ -121,11 +137,13 @@ class Constraint(RootModel): ) = Field(..., discriminator="type") -class CategoricalVariant(Entity, BaseModelForbidExtra): +class CategoricalVariant(CatVRSMetadataMixin, Entity, BaseModelForbidExtra): """A representation of a categorically-defined domain for variation, in which individual Constraintual variation instances may be members of the domain. """ + _maturity: ClassVar[Maturity] = Maturity.TRIAL_USE + type: Literal["CategoricalVariant"] = Field( default="CategoricalVariant", description="MUST be 'CategoricalVariant'" ) diff --git a/src/ga4gh/cat_vrs/recipes.py b/src/ga4gh/cat_vrs/recipes.py index 6b7ef79..4f8a083 100644 --- a/src/ga4gh/cat_vrs/recipes.py +++ b/src/ga4gh/cat_vrs/recipes.py @@ -4,6 +4,8 @@ the GA4GH website for more information. """ +from typing import ClassVar + from pydantic import Field, field_validator from ga4gh.cat_vrs.models import ( @@ -21,6 +23,7 @@ TRANSCRIBED_TO_RELATION, TRANSLATION_OF_RELATION, ) +from ga4gh.core.metadata import Maturity from ga4gh.core.models import MappableConcept @@ -34,6 +37,8 @@ class ProteinSequenceConsequence(CategoricalVariant): same altered codon(s). """ + _maturity: ClassVar[Maturity] = Maturity.TRIAL_USE + constraints: list[Constraint] = Field(..., min_length=1) @classmethod @@ -81,6 +86,8 @@ class CanonicalAllele(CategoricalVariant): associated cDNA transcript representations. """ + _maturity: ClassVar[Maturity] = Maturity.TRIAL_USE + constraints: list[Constraint] = Field(..., min_length=1, max_length=1) @classmethod @@ -126,6 +133,8 @@ def validate_constraints(cls, v: list[Constraint]) -> list[Constraint]: class CategoricalCnv(CategoricalVariant): """A representation of the constraints for matching knowledge about CNVs.""" + _maturity: ClassVar[Maturity] = Maturity.DRAFT + constraints: list[Constraint] = Field( ..., min_length=2, @@ -185,6 +194,8 @@ class FunctionVariant(CategoricalVariant): variants; e.g., gain-of-function or loss-of-function. """ + _maturity: ClassVar[Maturity] = Maturity.DRAFT + constraints: list[Constraint] = Field( ..., min_length=2, diff --git a/src/ga4gh/cat_vrs/version.py b/src/ga4gh/cat_vrs/version.py new file mode 100644 index 0000000..67f23f1 --- /dev/null +++ b/src/ga4gh/cat_vrs/version.py @@ -0,0 +1,3 @@ +"""Define Cat-VRS version""" + +CATVRS_VERSION = "1.1.0-snapshot.2026-02.3" diff --git a/tests/validation/test_model_metadata.py b/tests/validation/test_model_metadata.py new file mode 100644 index 0000000..0fac282 --- /dev/null +++ b/tests/validation/test_model_metadata.py @@ -0,0 +1,58 @@ +"""Test model metadata against the Cat-VRS source and JSON schemas.""" + +import json +from pathlib import Path + +import pytest +import yaml + +from ga4gh.cat_vrs import CATVRS_VERSION, models, recipes +from ga4gh.core.metadata import Maturity + +SCHEMA_DIR = Path(__file__).parents[2] / "submodules" / "cat_vrs" / "schema" / "cat-vrs" +SCHEMAS = ( + (models, SCHEMA_DIR / "cat-vrs-source.yaml"), + (recipes, SCHEMA_DIR / "recipes-source.yaml"), +) +JSON_DIR = SCHEMA_DIR / "json" + +with (SCHEMA_DIR / "cat-vrs-source.yaml").open() as source_file: + CATVRS_SOURCE = yaml.safe_load(source_file) + +SPEC_VERSION = CATVRS_SOURCE["$id"].split("/")[-2] + + +def _model_params(): + """Return model metadata discovered from both source YAML files.""" + params = [] + for model_module, source_path in SCHEMAS: + with source_path.open() as source_file: + definitions = yaml.safe_load(source_file)["$defs"] + for name, definition in definitions.items(): + model = getattr(model_module, name) + with (JSON_DIR / name).open() as schema_file: + schema = json.load(schema_file) + params.append(pytest.param(model, definition, schema, id=name)) + assert params, "No concrete Cat-VRS models discovered" + return params + + +def test_cat_vrs_version_matches_source_schema(): + """The package version matches the authoritative Cat-VRS source schema.""" + assert CATVRS_VERSION == SPEC_VERSION + + +@pytest.mark.parametrize(("model", "definition", "schema"), _model_params()) +def test_model_metadata(model, definition, schema): + """Model metadata matches its source and generated JSON Schemas.""" + expected_schema_id = ( + f"https://w3id.org/ga4gh/schema/cat-vrs/{SPEC_VERSION}/json/" + f"{model.__name__}" + ) + assert model.schema_id() == expected_schema_id + assert model.maturity() == Maturity(definition["maturity"]) + + generated_schema = model.model_json_schema() + assert generated_schema["$id"] == expected_schema_id + assert generated_schema["maturity"] == schema["maturity"] + assert "ga4gh" not in generated_schema