diff --git a/pyproject.toml b/pyproject.toml index 8d9c00c..3f27fd2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,55 +1,37 @@ [project] name = "ga4gh.va_spec" -authors = [ - {name = "Matt Brush"}, - {name = "Javier Lopez"}, -] +authors = [{ name = "Matt Brush" }, { name = "Javier Lopez" }] readme = "README.md" description = "GA4GH Variant Annotation (VA) 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.0-a1", - "ga4gh.cat_vrs~=0.8.0-a1", - "pydantic>=2.0,<3.0", - "typing_extensions", + "ga4gh.cat_vrs @ git+https://github.com/ga4gh/cat-vrs-python.git@30d6c5035fb7cd000bf637d8f03069633dd10ff1", + "pydantic>=2.0,<3.0", + "typing_extensions", ] [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/va-spec-python" @@ -64,53 +46,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 @@ -129,11 +110,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/va_spec/__init__.py b/src/ga4gh/va_spec/__init__.py index 3bbde77..9a95bb3 100644 --- a/src/ga4gh/va_spec/__init__.py +++ b/src/ga4gh/va_spec/__init__.py @@ -1,13 +1,15 @@ """Package for VA-Spec Python implementation""" -from importlib.metadata import PackageNotFoundError, version +from importlib.metadata import PackageNotFoundError +from importlib.metadata import version as package_version + +from ga4gh.va_spec.version import VASPEC_VERSION try: - __version__ = version(__name__) + __version__ = package_version(__name__) except PackageNotFoundError: # pragma: nocover __version__ = "unknown" finally: - del version, PackageNotFoundError - + del package_version, PackageNotFoundError -VASPEC_VERSION = "1.1.0-snapshot.2026-06.1" +__all__ = ["VASPEC_VERSION", "__version__"] diff --git a/src/ga4gh/va_spec/aac_2017/metadata.py b/src/ga4gh/va_spec/aac_2017/metadata.py new file mode 100644 index 0000000..431804d --- /dev/null +++ b/src/ga4gh/va_spec/aac_2017/metadata.py @@ -0,0 +1,9 @@ +"""Provide metadata for AAC 2017 VA-Spec models.""" + +from ga4gh.va_spec.metadata import VASpecMetadataMixin + + +class AAC2017MetadataMixin(VASpecMetadataMixin): + """Provide metadata shared by models in the AAC 2017 namespace.""" + + _schema_namespace = "aac-2017" diff --git a/src/ga4gh/va_spec/aac_2017/models.py b/src/ga4gh/va_spec/aac_2017/models.py index 52472e0..b840d64 100644 --- a/src/ga4gh/va_spec/aac_2017/models.py +++ b/src/ga4gh/va_spec/aac_2017/models.py @@ -6,12 +6,15 @@ from enum import Enum from types import MappingProxyType +from typing import ClassVar from pydantic import Field, RootModel, field_validator, model_validator from pydantic.dataclasses import dataclass from typing_extensions import Self +from ga4gh.core.metadata import Maturity from ga4gh.core.models import BaseModelForbidExtra, MappableConcept, iriReference +from ga4gh.va_spec.aac_2017.metadata import AAC2017MetadataMixin from ga4gh.va_spec.base.core import ( Direction, Document, @@ -58,9 +61,11 @@ class AmpAscoCapEvidenceLineStrength(str, Enum): ] -class AmpAscoCapEvidenceLine(EvidenceLine): +class AmpAscoCapEvidenceLine(AAC2017MetadataMixin, EvidenceLine): """Evidence line for AMP/ASCO/CAP""" + _maturity: ClassVar[Maturity] = Maturity.DRAFT + targetProposition: ( VariantPrognosticProposition | VariantDiagnosticProposition @@ -88,9 +93,13 @@ class _PrognosticEvidenceLineObject(AmpAscoCapEvidenceLine): targetProposition: VariantPrognosticProposition -class PrognosticEvidenceLine(RootModel[_PrognosticEvidenceLineObject | iriReference]): +class PrognosticEvidenceLine( + AAC2017MetadataMixin, RootModel[_PrognosticEvidenceLineObject | iriReference] +): """Prognostic evidence line for AMP/ASCO/CAP""" + _maturity: ClassVar[Maturity] = Maturity.DRAFT + class _DiagnosticEvidenceLineObject(AmpAscoCapEvidenceLine): """Internal diagnostic evidence line for AMP/ASCO/CAP""" @@ -98,9 +107,13 @@ class _DiagnosticEvidenceLineObject(AmpAscoCapEvidenceLine): targetProposition: VariantDiagnosticProposition -class DiagnosticEvidenceLine(RootModel[_DiagnosticEvidenceLineObject | iriReference]): +class DiagnosticEvidenceLine( + AAC2017MetadataMixin, RootModel[_DiagnosticEvidenceLineObject | iriReference] +): """Diagnostic evidence line for AMP/ASCO/CAP""" + _maturity: ClassVar[Maturity] = Maturity.DRAFT + class _TherapeuticEvidenceLineObject(AmpAscoCapEvidenceLine): """Internal therapeutic evidence line for AMP/ASCO/CAP""" @@ -108,9 +121,13 @@ class _TherapeuticEvidenceLineObject(AmpAscoCapEvidenceLine): targetProposition: VariantTherapeuticResponseProposition -class TherapeuticEvidenceLine(RootModel[_TherapeuticEvidenceLineObject | iriReference]): +class TherapeuticEvidenceLine( + AAC2017MetadataMixin, RootModel[_TherapeuticEvidenceLineObject | iriReference] +): """Therapeutic evidence line for AMP/ASCO/CAP""" + _maturity: ClassVar[Maturity] = Maturity.DRAFT + class AmpAscoCapStrengthCode(str, Enum): """Define constraints for AMP/ASCO/CAP strength coding""" @@ -177,12 +194,16 @@ class AmpAscoCapConfig: ) -class VariantClinicalSignificanceStatement(Statement, BaseModelForbidExtra): +class VariantClinicalSignificanceStatement( + AAC2017MetadataMixin, Statement, BaseModelForbidExtra +): """A statement reporting a conclusion from a single study about the clinical significance of a variant with respect to a condition, based on interpretation of the study's results. """ + _maturity: ClassVar[Maturity] = Maturity.DRAFT + proposition: VariantClinicalSignificanceProposition strength: MappableConcept | None = Field( default=None, diff --git a/src/ga4gh/va_spec/acmg_2015/metadata.py b/src/ga4gh/va_spec/acmg_2015/metadata.py new file mode 100644 index 0000000..f06e881 --- /dev/null +++ b/src/ga4gh/va_spec/acmg_2015/metadata.py @@ -0,0 +1,9 @@ +"""Provide metadata for ACMG 2015 VA-Spec models.""" + +from ga4gh.va_spec.metadata import VASpecMetadataMixin + + +class ACMG2015MetadataMixin(VASpecMetadataMixin): + """Provide metadata shared by models in the ACMG 2015 namespace.""" + + _schema_namespace = "acmg-2015" diff --git a/src/ga4gh/va_spec/acmg_2015/models.py b/src/ga4gh/va_spec/acmg_2015/models.py index b50dda6..98031da 100644 --- a/src/ga4gh/va_spec/acmg_2015/models.py +++ b/src/ga4gh/va_spec/acmg_2015/models.py @@ -10,7 +10,9 @@ from pydantic import Field, field_validator, model_validator from typing_extensions import Self +from ga4gh.core.metadata import Maturity from ga4gh.core.models import MappableConcept, iriReference +from ga4gh.va_spec.acmg_2015.metadata import ACMG2015MetadataMixin from ga4gh.va_spec.base.core import ( Direction, Document, @@ -62,7 +64,7 @@ class AcmgClassification(str, Enum): class VariantPathogenicityEvidenceLine( - EvidenceLine, MethodTypeCriterionValidationMixin + ACMG2015MetadataMixin, EvidenceLine, MethodTypeCriterionValidationMixin ): """An Evidence Line that describes how a specific type of information was interpreted as evidence for or against a variant's pathogenicity. In the ACMG @@ -71,6 +73,8 @@ class VariantPathogenicityEvidenceLine( adjusting the default strength based on the quality and abundance of evidence. """ + _maturity: ClassVar[Maturity] = Maturity.DRAFT + targetProposition: VariantPathogenicityProposition | None = Field( default=None, description="A Variant Pathogenicity Proposition against which a specific type of evidence was assessed, to determine the strength and direction of support this evidence provides for or against the proposition's validity.", @@ -320,9 +324,11 @@ def validate_model(self) -> Self: return self -class VariantPathogenicityStatement(Statement): +class VariantPathogenicityStatement(ACMG2015MetadataMixin, Statement): """A Statement describing the role of a variant in causing an inherited condition.""" + _maturity: ClassVar[Maturity] = Maturity.DRAFT + proposition: VariantPathogenicityProposition = Field( ..., description="A proposition about the pathogenicity of a variant, the validity of which is assessed and reported by the Statement. A Statement can put forth the proposition as being true, false, or uncertain, and may provide an assessment of the level of confidence/evidence supporting this claim.", diff --git a/src/ga4gh/va_spec/base/core.py b/src/ga4gh/va_spec/base/core.py index a5acfa6..03d20e5 100644 --- a/src/ga4gh/va_spec/base/core.py +++ b/src/ga4gh/va_spec/base/core.py @@ -5,7 +5,7 @@ from abc import ABC from datetime import date, datetime from enum import Enum -from typing import Annotated, Literal, TypeAlias +from typing import Annotated, ClassVar, Literal, TypeAlias from pydantic import ( ConfigDict, @@ -15,6 +15,7 @@ ) from ga4gh.cat_vrs.models import CategoricalVariant +from ga4gh.core.metadata import Maturity from ga4gh.core.models import ( BaseModelForbidExtra, Entity, @@ -28,6 +29,7 @@ System, TherapeuticResponsePredicate, ) +from ga4gh.va_spec.base.metadata import BaseMetadataMixin from ga4gh.va_spec.base.validators import ( validate_mappable_concept, ) @@ -47,12 +49,14 @@ class CoreType(str, Enum): STUDY_GROUP = "StudyGroup" -class Agent(Entity, BaseModelForbidExtra): +class Agent(BaseMetadataMixin, Entity, BaseModelForbidExtra): """An autonomous actor (person, organization, or software agent) that bears some form of responsibility for an activity taking place, for the existence of an entity, or for another agent's activity. """ + _maturity: ClassVar[Maturity] = Maturity.TRIAL_USE + type: Literal["Agent"] = Field( default=CoreType.AGENT.value, description=f"MUST be '{CoreType.AGENT.value}'." ) @@ -63,12 +67,14 @@ class Agent(Entity, BaseModelForbidExtra): ) -class Contribution(Entity, BaseModelForbidExtra): +class Contribution(BaseMetadataMixin, Entity, BaseModelForbidExtra): """An action taken by an agent in contributing to the creation, modification, assessment, or deprecation of a particular entity (e.g. a Statement, EvidenceLine, DataSet, Publication, etc.) """ + _maturity: ClassVar[Maturity] = Maturity.TRIAL_USE + type: Literal["Contribution"] = Field( default=CoreType.CONTRIBUTION.value, description=f"MUST be '{CoreType.CONTRIBUTION.value}'.", @@ -85,11 +91,13 @@ class Contribution(Entity, BaseModelForbidExtra): ) -class Document(Entity, BaseModelForbidExtra): +class Document(BaseMetadataMixin, Entity, BaseModelForbidExtra): """A collection of information, usually in a text-based or graphic human-readable form, intended to be read and understood together as a whole. """ + _maturity: ClassVar[Maturity] = Maturity.TRIAL_USE + type: Literal["Document"] = Field( default=CoreType.DOCUMENT.value, description=f"Must be '{CoreType.DOCUMENT.value}'", @@ -121,9 +129,11 @@ class Document(Entity, BaseModelForbidExtra): ) -class Method(Entity, BaseModelForbidExtra): +class Method(BaseMetadataMixin, Entity, BaseModelForbidExtra): """A set of instructions that specify how to achieve some objective.""" + _maturity: ClassVar[Maturity] = Maturity.TRIAL_USE + type: Literal["Method"] = Field( default=CoreType.METHOD.value, description=f"MUST be '{CoreType.METHOD.value}'." ) @@ -136,12 +146,14 @@ class Method(Entity, BaseModelForbidExtra): ) -class InformationEntity(Entity): +class InformationEntity(BaseMetadataMixin, Entity): """An abstract (non-physical) entity that represents 'information content' carried by physical or digital information artifacts such as books, web pages, data sets, or images. """ + _maturity: ClassVar[Maturity] = Maturity.TRIAL_USE + specifiedBy: Method | iriReference | None = Field( default=None, description="A specification that describes all or part of the process that led to creation of the Information Entity", @@ -156,11 +168,13 @@ class InformationEntity(Entity): ) -class DataSet(Entity, BaseModelForbidExtra): +class DataSet(BaseMetadataMixin, Entity, BaseModelForbidExtra): """A collection of related data items or records that are organized together in a common format or structure, to enable their computational manipulation as a unit. """ + _maturity: ClassVar[Maturity] = Maturity.TRIAL_USE + type: Literal["DataSet"] = Field( default=CoreType.DATA_SET.value, description=f"MUST be '{CoreType.DATA_SET.value}'.", @@ -186,13 +200,15 @@ class DataSet(Entity, BaseModelForbidExtra): ) -class StudyGroup(Entity, BaseModelForbidExtra): +class StudyGroup(BaseMetadataMixin, Entity, BaseModelForbidExtra): """A collection of individuals or specimens from the same taxonomic class, selected for analysis in a scientific study based on their exhibiting one or more common characteristics (e.g. species, race, age, gender, disease state, income). May be referred to as a 'cohort' or 'population' in specific research settings. """ + _maturity: ClassVar[Maturity] = Maturity.TRIAL_USE + type: Literal["StudyGroup"] = Field( default=CoreType.STUDY_GROUP.value, description=f"Must be '{CoreType.STUDY_GROUP.value}'", @@ -213,6 +229,8 @@ class _StudyResult(InformationEntity, ABC): describing how these data items were generated. """ + _maturity: ClassVar[Maturity] = Maturity.TRIAL_USE + sourceDataSet: DataSet | None = Field( default=None, description="A larger DataSet from which the data included in the StudyResult was taken or derived.", @@ -230,6 +248,8 @@ class _StudyResult(InformationEntity, ABC): class CohortAlleleFrequencyStudyResult(_StudyResult, BaseModelForbidExtra): """A StudyResult that reports measures related to the frequency of an Allele in a cohort""" + _maturity: ClassVar[Maturity] = Maturity.TRIAL_USE + type: Literal["CohortAlleleFrequencyStudyResult"] = Field( default="CohortAlleleFrequencyStudyResult", description="MUST be 'CohortAlleleFrequencyStudyResult'.", @@ -265,6 +285,8 @@ class TumorVariantFrequencyStudyResult(_StudyResult, BaseModelForbidExtra): across different tumor types. """ + _maturity: ClassVar[Maturity] = Maturity.DRAFT + type: Literal["TumorVariantFrequencyStudyResult"] = Field( default="TumorVariantFrequencyStudyResult", description="MUST be 'TumorVariantFrequencyStudyResult'.", @@ -304,6 +326,8 @@ class ExperimentalVariantFunctionalImpactStudyResult( ): """A StudyResult that reports a functional impact score from a variant functional assay or study.""" + _maturity: ClassVar[Maturity] = Maturity.TRIAL_USE + type: Literal["ExperimentalVariantFunctionalImpactStudyResult"] = Field( default="ExperimentalVariantFunctionalImpactStudyResult", description="MUST be 'ExperimentalVariantFunctionalImpactStudyResult'.", @@ -326,12 +350,14 @@ class ExperimentalVariantFunctionalImpactStudyResult( ) -class StudyResult(RootModel): +class StudyResult(BaseMetadataMixin, RootModel): """A collection of data items from a single study that pertain to a particular subject or experimental unit in the study, along with optional provenance information describing how these data items were generated. """ + _maturity: ClassVar[Maturity] = Maturity.TRIAL_USE + root: ( CohortAlleleFrequencyStudyResult | ExperimentalVariantFunctionalImpactStudyResult @@ -344,13 +370,15 @@ class StudyResult(RootModel): ) -class Proposition(Entity): +class Proposition(BaseMetadataMixin, Entity): """An abstract entity representing a possible fact that may be true or false. As abstract entities, Propositions capture a 'sharable' piece of meaning whose identify and existence is independent of space and time, or whether it is ever asserted to be true by some agent. """ + _maturity: ClassVar[Maturity] = Maturity.TRIAL_USE + subject: dict = Field( ..., description="The Entity or concept about which the Proposition is made." ) @@ -364,7 +392,9 @@ class Proposition(Entity): ) -class _SubjectVariantPropositionBase(Entity, ABC): +class _SubjectVariantPropositionBase(BaseMetadataMixin, Entity, ABC): + _maturity: ClassVar[Maturity] = Maturity.TRIAL_USE + subjectVariant: MolecularVariation | CategoricalVariant | iriReference = Field( ..., description="A variant that is the subject of the Proposition." ) @@ -373,6 +403,8 @@ class _SubjectVariantPropositionBase(Entity, ABC): class ClinicalVariantProposition(_SubjectVariantPropositionBase): """A proposition for use in describing the effect of variants in human subjects.""" + _maturity: ClassVar[Maturity] = Maturity.TRIAL_USE + geneContextQualifier: MappableConcept | iriReference | None = Field( default=None, description="Reports a gene impacted by the variant, which may contribute to the association described in the Proposition.", @@ -390,6 +422,8 @@ class ExperimentalVariantFunctionalImpactProposition( (typically a gene or gene product). """ + _maturity: ClassVar[Maturity] = Maturity.TRIAL_USE + type: Literal["ExperimentalVariantFunctionalImpactProposition"] = Field( default="ExperimentalVariantFunctionalImpactProposition", description="MUST be 'ExperimentalVariantFunctionalImpactProposition'.", @@ -415,6 +449,8 @@ class VariantClinicalSignificanceProposition( condition. """ + _maturity: ClassVar[Maturity] = Maturity.DRAFT + model_config = ConfigDict(use_enum_values=True) type: Literal["VariantClinicalSignificanceProposition"] = Field( @@ -437,6 +473,8 @@ class VariantDiagnosticProposition(ClinicalVariantProposition, BaseModelForbidEx model_config = ConfigDict(use_enum_values=True) + _maturity: ClassVar[Maturity] = Maturity.TRIAL_USE + type: Literal["VariantDiagnosticProposition"] = Field( default="VariantDiagnosticProposition", description="MUST be 'VariantDiagnosticProposition'.", @@ -453,6 +491,8 @@ class VariantDiagnosticProposition(ClinicalVariantProposition, BaseModelForbidEx class VariantOncogenicityProposition(ClinicalVariantProposition, BaseModelForbidExtra): """A proposition describing the role of a variant in causing a tumor type.""" + _maturity: ClassVar[Maturity] = Maturity.TRIAL_USE + type: Literal["VariantOncogenicityProposition"] = Field( default="VariantOncogenicityProposition", description="MUST be 'VariantOncogenicityProposition'.", @@ -469,6 +509,8 @@ class VariantOncogenicityProposition(ClinicalVariantProposition, BaseModelForbid class VariantPathogenicityProposition(ClinicalVariantProposition, BaseModelForbidExtra): """A proposition describing the role of a variant in causing a heritable condition.""" + _maturity: ClassVar[Maturity] = Maturity.TRIAL_USE + type: Literal["VariantPathogenicityProposition"] = Field( default="VariantPathogenicityProposition", description="Must be 'VariantPathogenicityProposition'", @@ -495,6 +537,8 @@ class VariantPrognosticProposition(ClinicalVariantProposition, BaseModelForbidEx model_config = ConfigDict(use_enum_values=True) + _maturity: ClassVar[Maturity] = Maturity.TRIAL_USE + type: Literal["VariantPrognosticProposition"] = Field( default="VariantPrognosticProposition", description="MUST be 'VariantPrognosticProposition'.", @@ -517,6 +561,8 @@ class VariantTherapeuticResponseProposition( model_config = ConfigDict(use_enum_values=True) + _maturity: ClassVar[Maturity] = Maturity.TRIAL_USE + type: Literal["VariantTherapeuticResponseProposition"] = Field( default="VariantTherapeuticResponseProposition", description="MUST be 'VariantTherapeuticResponseProposition'.", @@ -548,9 +594,11 @@ class VariantTherapeuticResponseProposition( ) -class SubjectVariantProposition(RootModel): +class SubjectVariantProposition(BaseMetadataMixin, RootModel): """A `Proposition` that has a variant as the subject.""" + _maturity: ClassVar[Maturity] = Maturity.TRIAL_USE + root: _SubjectVariantPropositionType = Field(discriminator="type") @@ -573,6 +621,8 @@ class EvidenceLine(InformationEntity, BaseModelForbidExtra): model_config = ConfigDict(use_enum_values=True) + _maturity: ClassVar[Maturity] = Maturity.TRIAL_USE + type: Literal["EvidenceLine"] = Field( default=CoreType.EVIDENCE_LINE.value, description=f"MUST be '{CoreType.EVIDENCE_LINE.value}'.", @@ -684,6 +734,8 @@ class Statement(InformationEntity, BaseModelForbidExtra): model_config = ConfigDict(use_enum_values=True) + _maturity: ClassVar[Maturity] = Maturity.TRIAL_USE + type: Literal["Statement"] = Field( default=CoreType.STATEMENT.value, description=f"MUST be '{CoreType.STATEMENT.value}'.", diff --git a/src/ga4gh/va_spec/base/domain_entities.py b/src/ga4gh/va_spec/base/domain_entities.py index 42ed83e..aa19db2 100644 --- a/src/ga4gh/va_spec/base/domain_entities.py +++ b/src/ga4gh/va_spec/base/domain_entities.py @@ -2,23 +2,29 @@ from __future__ import annotations +from typing import ClassVar + from pydantic import ConfigDict, Field, RootModel +from ga4gh.core.metadata import Maturity from ga4gh.core.models import ( BaseModelForbidExtra, Element, MappableConcept, MembershipOperator, ) +from ga4gh.va_spec.base.metadata import BaseMetadataMixin -class ConditionSet(Element, BaseModelForbidExtra): +class ConditionSet(BaseMetadataMixin, Element, BaseModelForbidExtra): """A set of conditions (diseases, phenotypes, traits) that occur together or are related, depending on the membership operator, and may manifest together in the same patient or individually in a different subset of participants in a research study. """ + _maturity: ClassVar[Maturity] = Maturity.TRIAL_USE + model_config = ConfigDict(use_enum_values=True) conditions: list[MappableConcept | ConditionSet] = Field( @@ -32,11 +38,13 @@ class ConditionSet(Element, BaseModelForbidExtra): ) -class Condition(RootModel): +class Condition(BaseMetadataMixin, RootModel): """A single condition (disease, phenotype, or trait), or a set of conditions (ConditionSet). """ + _maturity: ClassVar[Maturity] = Maturity.TRIAL_USE + root: ConditionSet | MappableConcept = Field( ..., json_schema_extra={ @@ -45,12 +53,14 @@ class Condition(RootModel): ) -class TherapyGroup(Element, BaseModelForbidExtra): +class TherapyGroup(BaseMetadataMixin, Element, BaseModelForbidExtra): """A group of two or more therapies that are applied in combination to a single patient/subject, or applied individually to a different subset of participants in a research study. """ + _maturity: ClassVar[Maturity] = Maturity.TRIAL_USE + model_config = ConfigDict(use_enum_values=True) therapies: list[MappableConcept] = Field( @@ -64,9 +74,11 @@ class TherapyGroup(Element, BaseModelForbidExtra): ) -class Therapeutic(RootModel): +class Therapeutic(BaseMetadataMixin, RootModel): """An individual therapy (drug, procedure, behavioral intervention, etc.), or group of therapies (TherapyGroup).""" + _maturity: ClassVar[Maturity] = Maturity.TRIAL_USE + root: TherapyGroup | MappableConcept = Field( ..., json_schema_extra={ diff --git a/src/ga4gh/va_spec/base/metadata.py b/src/ga4gh/va_spec/base/metadata.py new file mode 100644 index 0000000..7d4566e --- /dev/null +++ b/src/ga4gh/va_spec/base/metadata.py @@ -0,0 +1,9 @@ +"""Provide metadata for base VA-Spec models.""" + +from ga4gh.va_spec.metadata import VASpecMetadataMixin + + +class BaseMetadataMixin(VASpecMetadataMixin): + """Provide metadata shared by models in the base namespace.""" + + _schema_namespace = "base" diff --git a/src/ga4gh/va_spec/ccv_2022/metadata.py b/src/ga4gh/va_spec/ccv_2022/metadata.py new file mode 100644 index 0000000..4f17846 --- /dev/null +++ b/src/ga4gh/va_spec/ccv_2022/metadata.py @@ -0,0 +1,9 @@ +"""Provide metadata for CCV 2022 VA-Spec models.""" + +from ga4gh.va_spec.metadata import VASpecMetadataMixin + + +class CCV2022MetadataMixin(VASpecMetadataMixin): + """Provide metadata shared by models in the CCV 2022 namespace.""" + + _schema_namespace = "ccv-2022" diff --git a/src/ga4gh/va_spec/ccv_2022/models.py b/src/ga4gh/va_spec/ccv_2022/models.py index 039917b..3d198e1 100644 --- a/src/ga4gh/va_spec/ccv_2022/models.py +++ b/src/ga4gh/va_spec/ccv_2022/models.py @@ -10,6 +10,7 @@ from pydantic import Field, field_validator, model_validator from typing_extensions import Self +from ga4gh.core.metadata import Maturity from ga4gh.core.models import MappableConcept, iriReference from ga4gh.va_spec.base.core import ( Direction, @@ -29,6 +30,7 @@ MethodTypeCriterionValidationMixin, validate_mappable_concept, ) +from ga4gh.va_spec.ccv_2022.metadata import CCV2022MetadataMixin SYSTEM = System.CCV CCV_CODE_PATTERN = ( @@ -56,7 +58,9 @@ ) -class VariantOncogenicityEvidenceLine(EvidenceLine, MethodTypeCriterionValidationMixin): +class VariantOncogenicityEvidenceLine( + CCV2022MetadataMixin, EvidenceLine, MethodTypeCriterionValidationMixin +): """An Evidence Line that describes how evidence for a variant was interpreted to determine if a specific CCV 2022 criterion code is met, and the strength that evidence this provides for or against the variant's oncogenicity. An Evidence Line @@ -67,6 +71,8 @@ class VariantOncogenicityEvidenceLine(EvidenceLine, MethodTypeCriterionValidatio strength based on the quality and abundance of evidence. """ + _maturity: ClassVar[Maturity] = Maturity.DRAFT + targetProposition: VariantOncogenicityProposition | None = Field( default=None, description="A Variant Oncoogenicity Proposition against which a specific type of evidence was assessed, to determine the strength and direction of support this evidence provides for or against the proposition's validity.", @@ -248,12 +254,14 @@ def validate_model(self) -> Self: return self -class VariantOncogenicityStatement(Statement): +class VariantOncogenicityStatement(CCV2022MetadataMixin, Statement): """A statement reporting a conclusion from a single study about whether a variant is associated with oncogenicity (positive or negative) - based on interpretation of the study's results. """ + _maturity: ClassVar[Maturity] = Maturity.DRAFT + proposition: VariantOncogenicityProposition = Field( ..., description="A proposition about the oncogenicity of a variant, for which the study provides evidence. The validity of this proposition, and the level of confidence/evidence supporting it, may be assessed and reported by the Statement.", diff --git a/src/ga4gh/va_spec/metadata.py b/src/ga4gh/va_spec/metadata.py new file mode 100644 index 0000000..4ba2396 --- /dev/null +++ b/src/ga4gh/va_spec/metadata.py @@ -0,0 +1,22 @@ +"""Provide JSON Schema metadata for VA-Spec models.""" + +from typing import ClassVar + +from ga4gh.core.metadata import GKSMetadataMixin +from ga4gh.va_spec.version import VASPEC_VERSION + + +class VASpecMetadataMixin(GKSMetadataMixin): + """Expose metadata for a concrete VA-Spec model.""" + + _schema_namespace: ClassVar[str] + _product_name = "va-spec" + _product_version = VASPEC_VERSION + + @classmethod + def schema_id(cls) -> str: + """Return the model's canonical VA-Spec JSON Schema identifier.""" + return ( + f"{cls._schema_base_uri}/{cls._product_name}/{cls._product_version}/" + f"{cls._schema_namespace}/json/{cls.__name__}" + ) diff --git a/src/ga4gh/va_spec/version.py b/src/ga4gh/va_spec/version.py new file mode 100644 index 0000000..51d2060 --- /dev/null +++ b/src/ga4gh/va_spec/version.py @@ -0,0 +1,3 @@ +"""Define the VA-Spec version.""" + +VASPEC_VERSION = "1.1.0-snapshot.2026-06.1" diff --git a/tests/validation/test_model_metadata.py b/tests/validation/test_model_metadata.py new file mode 100644 index 0000000..1dd3241 --- /dev/null +++ b/tests/validation/test_model_metadata.py @@ -0,0 +1,69 @@ +"""Test model metadata against the VA-Spec source and JSON schemas.""" + +import json +from pathlib import Path + +import pytest +import yaml + +from ga4gh.core.metadata import Maturity +from ga4gh.va_spec import VASPEC_VERSION, base +from ga4gh.va_spec.aac_2017 import models as aac_2017 +from ga4gh.va_spec.acmg_2015 import models as acmg_2015 +from ga4gh.va_spec.ccv_2022 import models as ccv_2022 + +SCHEMA_DIR = Path(__file__).parents[2] / "submodules" / "va_spec" / "schema" / "va-spec" +SCHEMAS = ( + (base, "base", SCHEMA_DIR / "base" / "va-core-source.yaml"), + (base, "base", SCHEMA_DIR / "base" / "domain-entities-source.yaml"), + (aac_2017, "aac-2017", SCHEMA_DIR / "aac-2017" / "profile-source.yaml"), + (acmg_2015, "acmg-2015", SCHEMA_DIR / "acmg-2015" / "profile-source.yaml"), + (ccv_2022, "ccv-2022", SCHEMA_DIR / "ccv-2022" / "profile-source.yaml"), +) + + +def _model_params(): + """Return model metadata discovered from all VA-Spec source YAML files.""" + params = [] + for model_module, namespace, source_path in SCHEMAS: + with source_path.open() as source_file: + definitions = yaml.safe_load(source_file)["$defs"] + for name, definition in definitions.items(): + schema_path = SCHEMA_DIR / namespace / "json" / name + if not schema_path.exists(): + continue + model = getattr(model_module, name) + with schema_path.open() as schema_file: + schema = json.load(schema_file) + params.append(pytest.param(model, namespace, definition, schema, id=name)) + assert params, "No concrete VA-Spec models discovered" + return params + + +def test_va_spec_version_matches_source_schema(): + """The package version matches the authoritative VA-Spec source schema.""" + with SCHEMAS[0][2].open() as source_file: + source_id = yaml.safe_load(source_file)["$id"] + source_version = source_id.split("/va-spec/", maxsplit=1)[1].split("/", maxsplit=1)[ + 0 + ] + assert source_version == VASPEC_VERSION + + +@pytest.mark.parametrize( + ("model", "namespace", "definition", "schema"), _model_params() +) +def test_model_metadata(model, namespace, definition, schema): + """Model metadata matches its source and generated JSON Schemas.""" + expected_schema_id = ( + f"https://w3id.org/ga4gh/schema/va-spec/{VASPEC_VERSION}/{namespace}/json/" + f"{model.__name__}" + ) + assert model.schema_id() == expected_schema_id + assert "_maturity" in model.__dict__ + 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