From e6420f44de396eec8377b77c93d068f4966f485b Mon Sep 17 00:00:00 2001 From: James Kent Date: Thu, 15 Oct 2026 16:00:05 -0500 Subject: [PATCH] Ship the LinkML source with the package The LinkML YAML sat at the repo root, outside the package, so a consumer that depends on study_schema could reach it only through a git export. It now lives under src/study_schema/linkml/, shipped as package data, with study_schema.linkml.EXTRACTION, STORAGE and PAPER_PARSE as the paths of the three schemas. The extraction deviations and the extraction-to-storage map, which pondie checks the projection against, moved with them; storage-parameter-priorities.yaml is triage data and stays at the root. The generator reads its schemas through the same module, and a test builds a wheel and checks every YAML file is in it. Co-Authored-By: Claude Opus 5.5 --- README.md | 6 +- analysis-entities.md | 4 +- extraction-readme.md | 8 +- pyproject.toml | 1 + representing-models.md | 2 +- schema-tutorial.md | 6 +- src/study_schema/__init__.py | 1 + src/study_schema/linkml/__init__.py | 14 ++++ .../linkml/extraction-deviations.yaml | 4 +- .../linkml/extraction-evidence.yaml | 0 .../linkml/extraction-to-storage.map.yaml | 0 .../linkml/neuroimaging-paper-parse.yaml | 0 .../coordinates.yaml | 0 .../neuroimaging-paper-parse}/header.yaml | 0 .../neuroimaging-paper-parse}/paper.yaml | 0 .../parse_enums.yaml | 0 .../linkml/neuroimaging-study-extraction.yaml | 0 .../acquisition.yaml | 0 .../analysis.yaml | 0 .../analysis_details.yaml | 0 .../analysis_enums.yaml | 0 .../assessment.yaml | 0 .../design.yaml | 0 .../neuroimaging-study-extraction}/group.yaml | 0 .../region.yaml | 0 .../neuroimaging-study-extraction}/study.yaml | 0 .../neuroimaging-study-extraction}/task.yaml | 0 .../linkml/neuroimaging-study-storage.yaml | 0 .../acquisition.yaml | 0 .../neuroimaging-study-storage}/analysis.yaml | 0 .../analysis_details.yaml | 0 .../analysis_enums.yaml | 0 .../assessment.yaml | 0 .../neuroimaging-study-storage}/design.yaml | 0 .../neuroimaging-study-storage}/group.yaml | 0 .../neuroimaging-study-storage}/region.yaml | 0 .../neuroimaging-study-storage}/study.yaml | 0 .../neuroimaging-study-storage}/subsets.yaml | 0 .../neuroimaging-study-storage}/task.yaml | 0 storage-schema-design-notes.md | 4 +- tests/test_linkml.py | 82 +++++++++++++++++++ tests/test_statistics.py | 5 +- tests/test_unique_keys.py | 2 +- tools/generate_models.py | 14 ++-- 44 files changed, 127 insertions(+), 26 deletions(-) create mode 100644 src/study_schema/linkml/__init__.py rename extraction-deviations.yaml => src/study_schema/linkml/extraction-deviations.yaml (99%) rename extraction-evidence.yaml => src/study_schema/linkml/extraction-evidence.yaml (100%) rename extraction-to-storage.map.yaml => src/study_schema/linkml/extraction-to-storage.map.yaml (100%) rename neuroimaging-paper-parse.yaml => src/study_schema/linkml/neuroimaging-paper-parse.yaml (100%) rename {neuroimaging-paper-parse => src/study_schema/linkml/neuroimaging-paper-parse}/coordinates.yaml (100%) rename {neuroimaging-paper-parse => src/study_schema/linkml/neuroimaging-paper-parse}/header.yaml (100%) rename {neuroimaging-paper-parse => src/study_schema/linkml/neuroimaging-paper-parse}/paper.yaml (100%) rename {neuroimaging-paper-parse => src/study_schema/linkml/neuroimaging-paper-parse}/parse_enums.yaml (100%) rename neuroimaging-study-extraction.yaml => src/study_schema/linkml/neuroimaging-study-extraction.yaml (100%) rename {neuroimaging-study-extraction => src/study_schema/linkml/neuroimaging-study-extraction}/acquisition.yaml (100%) rename {neuroimaging-study-extraction => src/study_schema/linkml/neuroimaging-study-extraction}/analysis.yaml (100%) rename {neuroimaging-study-extraction => src/study_schema/linkml/neuroimaging-study-extraction}/analysis_details.yaml (100%) rename {neuroimaging-study-extraction => src/study_schema/linkml/neuroimaging-study-extraction}/analysis_enums.yaml (100%) rename {neuroimaging-study-extraction => src/study_schema/linkml/neuroimaging-study-extraction}/assessment.yaml (100%) rename {neuroimaging-study-extraction => src/study_schema/linkml/neuroimaging-study-extraction}/design.yaml (100%) rename {neuroimaging-study-extraction => src/study_schema/linkml/neuroimaging-study-extraction}/group.yaml (100%) rename {neuroimaging-study-extraction => src/study_schema/linkml/neuroimaging-study-extraction}/region.yaml (100%) rename {neuroimaging-study-extraction => src/study_schema/linkml/neuroimaging-study-extraction}/study.yaml (100%) rename {neuroimaging-study-extraction => src/study_schema/linkml/neuroimaging-study-extraction}/task.yaml (100%) rename neuroimaging-study-storage.yaml => src/study_schema/linkml/neuroimaging-study-storage.yaml (100%) rename {neuroimaging-study-storage => src/study_schema/linkml/neuroimaging-study-storage}/acquisition.yaml (100%) rename {neuroimaging-study-storage => src/study_schema/linkml/neuroimaging-study-storage}/analysis.yaml (100%) rename {neuroimaging-study-storage => src/study_schema/linkml/neuroimaging-study-storage}/analysis_details.yaml (100%) rename {neuroimaging-study-storage => src/study_schema/linkml/neuroimaging-study-storage}/analysis_enums.yaml (100%) rename {neuroimaging-study-storage => src/study_schema/linkml/neuroimaging-study-storage}/assessment.yaml (100%) rename {neuroimaging-study-storage => src/study_schema/linkml/neuroimaging-study-storage}/design.yaml (100%) rename {neuroimaging-study-storage => src/study_schema/linkml/neuroimaging-study-storage}/group.yaml (100%) rename {neuroimaging-study-storage => src/study_schema/linkml/neuroimaging-study-storage}/region.yaml (100%) rename {neuroimaging-study-storage => src/study_schema/linkml/neuroimaging-study-storage}/study.yaml (100%) rename {neuroimaging-study-storage => src/study_schema/linkml/neuroimaging-study-storage}/subsets.yaml (100%) rename {neuroimaging-study-storage => src/study_schema/linkml/neuroimaging-study-storage}/task.yaml (100%) create mode 100644 tests/test_linkml.py diff --git a/README.md b/README.md index 6cff9cd..d8161db 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ runs, the extraction pipeline, and the three modules that define what a record m submodule, so a schema change and the code that consumes it are versioned together without either being a copy of the other. -`neuroimaging-study-storage.yaml` and the modules under `neuroimaging-study-storage/` are +The LinkML lives under `src/study_schema/linkml/`. `neuroimaging-study-storage.yaml` and the modules under `neuroimaging-study-storage/` are the full storage schema: everything we might ever want to represent. It is the source of truth and is never narrowed in place. @@ -282,6 +282,8 @@ from study_schema.models.paper_parse import CoordinateParse parse = CoordinateParse.model_validate_json(path.read_bytes()) ``` +**The LinkML itself.** The YAML ships inside the package under `study_schema.linkml`, so a consumer that needs the source reads it from the installed copy rather than from a checkout: `study_schema.linkml.EXTRACTION`, `STORAGE` and `PAPER_PARSE` are `pathlib.Path`s to the three top-level schemas, which import their modules by relative path, and `study_schema.linkml.EXTRACTION.parent` is the directory they sit in. The contract files that pondie checks, `extraction-deviations.yaml` and `extraction-to-storage.map.yaml`, are packaged alongside them. `storage-parameter-priorities.yaml` is reviewer triage data and is not packaged. `tools/generate_models.py` reads the same files, so the copy a consumer holds is the one the committed models were generated from. + **Reference properties.** A string property that holds another entity's `local_id` carries `"x-reference": ""` on the property itself, beside its `type`; for a multivalued slot that property is the array, not its `items`. The keyword is set from the LinkML induced slots, so `is_a` and `slot_usage` apply, and only where the range class has an identifier and the slot is not inlined. JSON Schema validators ignore it, and `study_schema.jsonschema.references(name)` returns the same relationships as `Reference(cls, slot, target, multivalued)` named tuples, sorted by class and slot. **Unique keys.** A class with a LinkML `unique_keys` carries `"x-unique-keys": [[slot, ...], ...]` on its definition, its own keys first and then inherited ones with each key's slots in declared order, and `study_schema.jsonschema.unique_keys(name)` returns them as a dict from class to tuples of slot names. @@ -388,7 +390,7 @@ the priority inventory from here. |---|---| | [schema-tutorial.md](schema-tutorial.md) | The course: twelve chapters teaching the whole schema — what every entity is for, what each field holds and what it must not be confused with, the rules for filling a record, and the reasoning steps to take when a paper is ambiguous. Start here if you are new to the schema; the documents below are the references it teaches from | | [extraction-readme.md](extraction-readme.md) | Rules the schema cannot state: the gates that skip a paper, extraction conventions, validator invariants, mapper responsibilities, and known limits | -| [extraction-deviations.yaml](extraction-deviations.yaml) | Every way the extraction schema is not a projection of storage. Currently: pipeline provenance, and nothing else. Its trailing comment lists the six shapes the hand-written schema used to have, as candidates to re-test | +| [extraction-deviations.yaml](src/study_schema/linkml/extraction-deviations.yaml) | Every way the extraction schema is not a projection of storage. Currently: pipeline provenance, and nothing else. Its trailing comment lists the six shapes the hand-written schema used to have, as candidates to re-test | | [storage-schema-design-notes.md](storage-schema-design-notes.md) | Why the storage schema is shaped the way it is | | [analysis-entities.md](analysis-entities.md) | What each entity around an analysis is called, what owns it, and what points at what: the three layers, a crosswalk from the words papers use, and the joins a synthesis reads a record back out through. Start here before representing-models.md | | [representing-models.md](representing-models.md) | How to put a reported analysis into the schema: which facts belong to the model and which to the contrast, where each class's job ends, and what to do when a paper does not divide things the way the schema does. Its YAML fragments and the worked records under `pondie/tests/fixtures/examples/` are checked on every test run | diff --git a/analysis-entities.md b/analysis-entities.md index f508efc..ed0ac3b 100644 --- a/analysis-entities.md +++ b/analysis-entities.md @@ -4,7 +4,7 @@ This document names each object around an `Analysis` and shows its ownership and This is the map, not the instructions. [representing-models.md](representing-models.md) says how to encode a reported result and what to do when a paper resists; -[neuroimaging-study-storage/](neuroimaging-study-storage/) says what each field is and how to fill +[neuroimaging-study-storage/](src/study_schema/linkml/neuroimaging-study-storage/) says what each field is and how to fill it. Read this first if the class names do not yet mean anything, or if you know the statistics and want to know which of these words the schema has borrowed for something narrower. @@ -265,6 +265,6 @@ acquired count when the analysis dropped participants. | The four values of `direction`, and the three ways to be non-directional | [§4](representing-models.md#4-the-four-values-of-direction-and-the-three-ways-to-be-non-directional) | | Twelve worked records, from a simple contrast to a two-stage model | [§5](representing-models.md#5-worked-models) | | A paper that does not divide the world the way the schema does | [§6](representing-models.md#6-when-the-paper-does-not-fit) | -| What each field is, and how to fill it | [neuroimaging-study-storage/](neuroimaging-study-storage/) | +| What each field is, and how to fill it | [neuroimaging-study-storage/](src/study_schema/linkml/neuroimaging-study-storage/) | | The rest of the record — cohorts, paradigm, acquisitions — taught rather than mapped | [schema-tutorial.md](schema-tutorial.md) | | Why the schema is shaped this way | [storage-schema-design-notes.md](storage-schema-design-notes.md) | diff --git a/extraction-readme.md b/extraction-readme.md index d256a45..f10d865 100644 --- a/extraction-readme.md +++ b/extraction-readme.md @@ -185,7 +185,7 @@ The extraction schema used to carry eight sibling slots here and five on `Acquis "all eight empty" standing in for mass-univariate. Naming the type is one field instead of eight and cannot be silently ambiguous, but it does ask the extractor to make a choice it previously made by omission. If that turns out to extract worse, it is candidate 2 in -[extraction-deviations.yaml](extraction-deviations.yaml). +[extraction-deviations.yaml](src/study_schema/linkml/extraction-deviations.yaml). ### A task's name is split into its paradigm and the ways it varies @@ -265,7 +265,7 @@ recorded verbatim and a 23-entry synonym table in the map did the translating. Extraction used to spread direction across five per-axis slots, one each for group, condition, timepoint, arm and factor, because comparative wording is per axis and that is what an extractor can point at. Storage always had the one slot, and now so does extraction. It is -candidate 3 in [extraction-deviations.yaml](extraction-deviations.yaml) — the biggest of the +candidate 3 in [extraction-deviations.yaml](src/study_schema/linkml/extraction-deviations.yaml) — the biggest of the six, and the one most worth measuring, since the collapse asks the extractor to resolve an entity to the model term whose level names it *and* to pick a direction in the same step. @@ -676,7 +676,7 @@ impossible by construction rather than merely documented, and copies of the real schemas, because a completeness check that cannot fail is worth nothing. The shapes that used to need absorbing are listed as candidates at the end of -[extraction-deviations.yaml](extraction-deviations.yaml). They were not obviously wrong -- +[extraction-deviations.yaml](src/study_schema/linkml/extraction-deviations.yaml). They were not obviously wrong -- they were chosen because they read more naturally to an extractor -- and the point of starting from the projection is to find out which of them actually earn their cost. Adding one back is an entry in that file plus a matching entry in the map. @@ -729,7 +729,7 @@ extraction schema still asks for them. `ConnectivityEdge.directionality` is `deterministic` without any outside source: the mapper looks it up from the containing analysis's `connectivity_method` through `value_map` in -[extraction-to-storage.map.yaml](extraction-to-storage.map.yaml). Only DCM and Granger causality +[extraction-to-storage.map.yaml](src/study_schema/linkml/extraction-to-storage.map.yaml). Only DCM and Granger causality support a claim about which region influences which, and no wording of the paper changes that, so the field is not one to ask a reader for. It is left unset where the method is written as free text, since there is nothing to look up — the one case where an edge's directionality is genuinely unknown diff --git a/pyproject.toml b/pyproject.toml index 9cfbc2a..50c6871 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,6 +26,7 @@ where = ["src"] [tool.setuptools.package-data] "study_schema" = ["py.typed"] "study_schema.jsonschema" = ["*.schema.json"] +"study_schema.linkml" = ["*.yaml", "*/*.yaml"] [tool.pytest.ini_options] testpaths = ["tests"] diff --git a/representing-models.md b/representing-models.md index 557f68d..bf53882 100644 --- a/representing-models.md +++ b/representing-models.md @@ -3,7 +3,7 @@ How to put a reported analysis into the storage schema, and how to tell which shape it is. This is the practical companion to [storage-schema-design-notes.md](storage-schema-design-notes.md), which holds the reasoning behind the shape rather than instructions for using it. The schema's field -definitions are in [neuroimaging-study-storage/](neuroimaging-study-storage/) and say what each +definitions are in [neuroimaging-study-storage/](src/study_schema/linkml/neuroimaging-study-storage/) and say what each field *is*; this says how the classes fit together and what to do when a paper does not divide the world the way the schema does. If the class names are not yet familiar, [analysis-entities.md](analysis-entities.md) is the map: what each entity is called, what owns it, diff --git a/schema-tutorial.md b/schema-tutorial.md index cec0cf9..3083f81 100644 --- a/schema-tutorial.md +++ b/schema-tutorial.md @@ -5,7 +5,7 @@ connect, what each field holds and what it must not be confused with, and how to is ambiguous. This is the teaching document. The schema files under -[neuroimaging-study-storage/](neuroimaging-study-storage/) are the source of truth for what a field +[neuroimaging-study-storage/](src/study_schema/linkml/neuroimaging-study-storage/) are the source of truth for what a field holds, and they win any disagreement with what follows. --- @@ -1795,11 +1795,11 @@ required column leaves out `id`, which every class that has one requires; "Field ## Appendix B. Where everything is **The schema.** `neuroimaging-study-storage.yaml` plus eleven modules under -[neuroimaging-study-storage/](neuroimaging-study-storage/): `subsets`, `design`, `study`, `group`, +[neuroimaging-study-storage/](src/study_schema/linkml/neuroimaging-study-storage/): `subsets`, `design`, `study`, `group`, `task`, `assessment`, `region`, `acquisition`, `analysis_enums`, `analysis_details`, `analysis`. `analysis_enums` holds 27 of the 38 vocabularies, separately so that the analysis and analysis-details modules can both bind to them without either importing the other. -[neuroimaging-study-extraction/](neuroimaging-study-extraction/) is the generated projection and is +[neuroimaging-study-extraction/](src/study_schema/linkml/neuroimaging-study-extraction/) is the generated projection and is never hand-edited. **The uniqueness rules**, each saying "recorded twice is one thing": `ModelTerm.name` within its diff --git a/src/study_schema/__init__.py b/src/study_schema/__init__.py index a9292dc..6e88213 100644 --- a/src/study_schema/__init__.py +++ b/src/study_schema/__init__.py @@ -4,6 +4,7 @@ study_schema.models.extraction Study (an extraction record) pondie -> ingester study_schema.models.storage Study (the storage shape) ingester -> neurostore study_schema.jsonschema the same contracts as JSON Schema, for non-Python readers + study_schema.linkml the LinkML source, as paths into the installed package study_schema.keys the analysis key rule study_schema.statistics which statistic kinds are signed; a point's side study_schema.layouts where those files sit on disk (needs the `layouts` extra) diff --git a/src/study_schema/linkml/__init__.py b/src/study_schema/linkml/__init__.py new file mode 100644 index 0000000..89045d0 --- /dev/null +++ b/src/study_schema/linkml/__init__.py @@ -0,0 +1,14 @@ +"""The LinkML schemas the package is generated from, as paths into the installed package. + + EXTRACTION neuroimaging-study-extraction.yaml pondie -> ingester + STORAGE neuroimaging-study-storage.yaml ingester -> neurostore + PAPER_PARSE neuroimaging-paper-parse.yaml ingestion -> pondie, ingester +""" + +from pathlib import Path + +_DIRECTORY = Path(__file__).parent + +EXTRACTION = _DIRECTORY / "neuroimaging-study-extraction.yaml" +STORAGE = _DIRECTORY / "neuroimaging-study-storage.yaml" +PAPER_PARSE = _DIRECTORY / "neuroimaging-paper-parse.yaml" diff --git a/extraction-deviations.yaml b/src/study_schema/linkml/extraction-deviations.yaml similarity index 99% rename from extraction-deviations.yaml rename to src/study_schema/linkml/extraction-deviations.yaml index 0fcc63b..2a86d14 100644 --- a/extraction-deviations.yaml +++ b/src/study_schema/linkml/extraction-deviations.yaml @@ -1,7 +1,7 @@ # extraction-deviations.yaml # # Everything in the extraction schema that is not a mechanical projection of the storage -# schema. gen_extraction_schema.py applies this file on top of the projection; nothing +# schema. pondie.schema.generate applies this file on top of the projection; nothing # else may differ, and check_extraction_mirrors_storage.py enforces that. # # The file has two parts, and the difference between them matters. @@ -35,7 +35,7 @@ header: Immutable, evidence-first structured output from extracting a neuroimaging publication. - This schema is generated from neuroimaging-study-storage by gen_extraction_schema.py. + This schema is generated from neuroimaging-study-storage by pondie.schema.generate. It has the same classes with the same slots in the same nesting; what differs is that every source-derived value is wrapped. An ExtractedValue records whether a value was found (extraction_status), the value itself when it was, how it relates to the source diff --git a/extraction-evidence.yaml b/src/study_schema/linkml/extraction-evidence.yaml similarity index 100% rename from extraction-evidence.yaml rename to src/study_schema/linkml/extraction-evidence.yaml diff --git a/extraction-to-storage.map.yaml b/src/study_schema/linkml/extraction-to-storage.map.yaml similarity index 100% rename from extraction-to-storage.map.yaml rename to src/study_schema/linkml/extraction-to-storage.map.yaml diff --git a/neuroimaging-paper-parse.yaml b/src/study_schema/linkml/neuroimaging-paper-parse.yaml similarity index 100% rename from neuroimaging-paper-parse.yaml rename to src/study_schema/linkml/neuroimaging-paper-parse.yaml diff --git a/neuroimaging-paper-parse/coordinates.yaml b/src/study_schema/linkml/neuroimaging-paper-parse/coordinates.yaml similarity index 100% rename from neuroimaging-paper-parse/coordinates.yaml rename to src/study_schema/linkml/neuroimaging-paper-parse/coordinates.yaml diff --git a/neuroimaging-paper-parse/header.yaml b/src/study_schema/linkml/neuroimaging-paper-parse/header.yaml similarity index 100% rename from neuroimaging-paper-parse/header.yaml rename to src/study_schema/linkml/neuroimaging-paper-parse/header.yaml diff --git a/neuroimaging-paper-parse/paper.yaml b/src/study_schema/linkml/neuroimaging-paper-parse/paper.yaml similarity index 100% rename from neuroimaging-paper-parse/paper.yaml rename to src/study_schema/linkml/neuroimaging-paper-parse/paper.yaml diff --git a/neuroimaging-paper-parse/parse_enums.yaml b/src/study_schema/linkml/neuroimaging-paper-parse/parse_enums.yaml similarity index 100% rename from neuroimaging-paper-parse/parse_enums.yaml rename to src/study_schema/linkml/neuroimaging-paper-parse/parse_enums.yaml diff --git a/neuroimaging-study-extraction.yaml b/src/study_schema/linkml/neuroimaging-study-extraction.yaml similarity index 100% rename from neuroimaging-study-extraction.yaml rename to src/study_schema/linkml/neuroimaging-study-extraction.yaml diff --git a/neuroimaging-study-extraction/acquisition.yaml b/src/study_schema/linkml/neuroimaging-study-extraction/acquisition.yaml similarity index 100% rename from neuroimaging-study-extraction/acquisition.yaml rename to src/study_schema/linkml/neuroimaging-study-extraction/acquisition.yaml diff --git a/neuroimaging-study-extraction/analysis.yaml b/src/study_schema/linkml/neuroimaging-study-extraction/analysis.yaml similarity index 100% rename from neuroimaging-study-extraction/analysis.yaml rename to src/study_schema/linkml/neuroimaging-study-extraction/analysis.yaml diff --git a/neuroimaging-study-extraction/analysis_details.yaml b/src/study_schema/linkml/neuroimaging-study-extraction/analysis_details.yaml similarity index 100% rename from neuroimaging-study-extraction/analysis_details.yaml rename to src/study_schema/linkml/neuroimaging-study-extraction/analysis_details.yaml diff --git a/neuroimaging-study-extraction/analysis_enums.yaml b/src/study_schema/linkml/neuroimaging-study-extraction/analysis_enums.yaml similarity index 100% rename from neuroimaging-study-extraction/analysis_enums.yaml rename to src/study_schema/linkml/neuroimaging-study-extraction/analysis_enums.yaml diff --git a/neuroimaging-study-extraction/assessment.yaml b/src/study_schema/linkml/neuroimaging-study-extraction/assessment.yaml similarity index 100% rename from neuroimaging-study-extraction/assessment.yaml rename to src/study_schema/linkml/neuroimaging-study-extraction/assessment.yaml diff --git a/neuroimaging-study-extraction/design.yaml b/src/study_schema/linkml/neuroimaging-study-extraction/design.yaml similarity index 100% rename from neuroimaging-study-extraction/design.yaml rename to src/study_schema/linkml/neuroimaging-study-extraction/design.yaml diff --git a/neuroimaging-study-extraction/group.yaml b/src/study_schema/linkml/neuroimaging-study-extraction/group.yaml similarity index 100% rename from neuroimaging-study-extraction/group.yaml rename to src/study_schema/linkml/neuroimaging-study-extraction/group.yaml diff --git a/neuroimaging-study-extraction/region.yaml b/src/study_schema/linkml/neuroimaging-study-extraction/region.yaml similarity index 100% rename from neuroimaging-study-extraction/region.yaml rename to src/study_schema/linkml/neuroimaging-study-extraction/region.yaml diff --git a/neuroimaging-study-extraction/study.yaml b/src/study_schema/linkml/neuroimaging-study-extraction/study.yaml similarity index 100% rename from neuroimaging-study-extraction/study.yaml rename to src/study_schema/linkml/neuroimaging-study-extraction/study.yaml diff --git a/neuroimaging-study-extraction/task.yaml b/src/study_schema/linkml/neuroimaging-study-extraction/task.yaml similarity index 100% rename from neuroimaging-study-extraction/task.yaml rename to src/study_schema/linkml/neuroimaging-study-extraction/task.yaml diff --git a/neuroimaging-study-storage.yaml b/src/study_schema/linkml/neuroimaging-study-storage.yaml similarity index 100% rename from neuroimaging-study-storage.yaml rename to src/study_schema/linkml/neuroimaging-study-storage.yaml diff --git a/neuroimaging-study-storage/acquisition.yaml b/src/study_schema/linkml/neuroimaging-study-storage/acquisition.yaml similarity index 100% rename from neuroimaging-study-storage/acquisition.yaml rename to src/study_schema/linkml/neuroimaging-study-storage/acquisition.yaml diff --git a/neuroimaging-study-storage/analysis.yaml b/src/study_schema/linkml/neuroimaging-study-storage/analysis.yaml similarity index 100% rename from neuroimaging-study-storage/analysis.yaml rename to src/study_schema/linkml/neuroimaging-study-storage/analysis.yaml diff --git a/neuroimaging-study-storage/analysis_details.yaml b/src/study_schema/linkml/neuroimaging-study-storage/analysis_details.yaml similarity index 100% rename from neuroimaging-study-storage/analysis_details.yaml rename to src/study_schema/linkml/neuroimaging-study-storage/analysis_details.yaml diff --git a/neuroimaging-study-storage/analysis_enums.yaml b/src/study_schema/linkml/neuroimaging-study-storage/analysis_enums.yaml similarity index 100% rename from neuroimaging-study-storage/analysis_enums.yaml rename to src/study_schema/linkml/neuroimaging-study-storage/analysis_enums.yaml diff --git a/neuroimaging-study-storage/assessment.yaml b/src/study_schema/linkml/neuroimaging-study-storage/assessment.yaml similarity index 100% rename from neuroimaging-study-storage/assessment.yaml rename to src/study_schema/linkml/neuroimaging-study-storage/assessment.yaml diff --git a/neuroimaging-study-storage/design.yaml b/src/study_schema/linkml/neuroimaging-study-storage/design.yaml similarity index 100% rename from neuroimaging-study-storage/design.yaml rename to src/study_schema/linkml/neuroimaging-study-storage/design.yaml diff --git a/neuroimaging-study-storage/group.yaml b/src/study_schema/linkml/neuroimaging-study-storage/group.yaml similarity index 100% rename from neuroimaging-study-storage/group.yaml rename to src/study_schema/linkml/neuroimaging-study-storage/group.yaml diff --git a/neuroimaging-study-storage/region.yaml b/src/study_schema/linkml/neuroimaging-study-storage/region.yaml similarity index 100% rename from neuroimaging-study-storage/region.yaml rename to src/study_schema/linkml/neuroimaging-study-storage/region.yaml diff --git a/neuroimaging-study-storage/study.yaml b/src/study_schema/linkml/neuroimaging-study-storage/study.yaml similarity index 100% rename from neuroimaging-study-storage/study.yaml rename to src/study_schema/linkml/neuroimaging-study-storage/study.yaml diff --git a/neuroimaging-study-storage/subsets.yaml b/src/study_schema/linkml/neuroimaging-study-storage/subsets.yaml similarity index 100% rename from neuroimaging-study-storage/subsets.yaml rename to src/study_schema/linkml/neuroimaging-study-storage/subsets.yaml diff --git a/neuroimaging-study-storage/task.yaml b/src/study_schema/linkml/neuroimaging-study-storage/task.yaml similarity index 100% rename from neuroimaging-study-storage/task.yaml rename to src/study_schema/linkml/neuroimaging-study-storage/task.yaml diff --git a/storage-schema-design-notes.md b/storage-schema-design-notes.md index 4c6903f..ea3ee71 100644 --- a/storage-schema-design-notes.md +++ b/storage-schema-design-notes.md @@ -2,7 +2,7 @@ Why the storage schema is shaped the way it is: the reasoning behind a decision, the alternatives weighed against it, and the measurement that settled it. The field -definitions themselves are in [neuroimaging-study-storage/](neuroimaging-study-storage/) +definitions themselves are in [neuroimaging-study-storage/](src/study_schema/linkml/neuroimaging-study-storage/) and say what each field *is*; [representing-models.md](representing-models.md) says how to use them; [analysis-entities.md](analysis-entities.md) says what owns what. This holds the part none of those should carry. @@ -407,7 +407,7 @@ it is asking for a chance to disagree with the method, and the corpus took the c on 2026-08-14, 16 sat on `dcm` and **five of those were marked `undirected`**, which DCM cannot be. It therefore became a lookup in `derivations.ConnectivityEdge.directionality.value_map` in -[extraction-to-storage.map.yaml](extraction-to-storage.map.yaml) — two directed values, six +[extraction-to-storage.map.yaml](src/study_schema/linkml/extraction-to-storage.map.yaml) — two directed values, six undirected, one line each. ### What the derivation cannot do diff --git a/tests/test_linkml.py b/tests/test_linkml.py new file mode 100644 index 0000000..4c429bb --- /dev/null +++ b/tests/test_linkml.py @@ -0,0 +1,82 @@ +from __future__ import annotations + +import shutil +import subprocess +import sys +import zipfile +from pathlib import Path + +import pytest +import yaml + +from study_schema import linkml + +ROOT = Path(__file__).resolve().parents[1] +LINKML = linkml.EXTRACTION.parent.resolve() +SCHEMAS = ("EXTRACTION", "STORAGE", "PAPER_PARSE") + + +def _imports(path: Path) -> list[str]: + document = yaml.safe_load(path.read_text("utf-8")) or {} + return [entry for entry in document.get("imports", []) if not entry.startswith("linkml:")] + + +@pytest.mark.parametrize("name", SCHEMAS) +def test_constant_points_at_a_packaged_schema(name): + schema = getattr(linkml, name) + + assert isinstance(schema, Path) + assert schema.is_file() + assert schema.resolve().is_relative_to(LINKML) + + +@pytest.mark.parametrize("name", SCHEMAS) +def test_imports_resolve_inside_the_packaged_tree(name): + pending = [getattr(linkml, name)] + seen = set() + while pending: + current = pending.pop() + if current in seen: + continue + seen.add(current) + for target in _imports(current): + imported = (current.parent / f"{target}.yaml").resolve() + assert imported.is_file(), f"{current.name} imports {target!r}, which is not packaged" + assert imported.is_relative_to(LINKML) + pending.append(imported) + assert len(seen) > 1 + + +def test_wheel_ships_the_linkml_tree(tmp_path): + pytest.importorskip("setuptools.build_meta", reason="building a wheel needs setuptools") + project = tmp_path / "project" + project.mkdir() + for name in ("pyproject.toml", "README.md"): + shutil.copy(ROOT / name, project / name) + shutil.copytree(ROOT / "src", project / "src") + wheels = tmp_path / "wheels" + subprocess.run( + [ + sys.executable, + "-c", + "import sys; from setuptools import build_meta; print(build_meta.build_wheel(sys.argv[1]))", + str(wheels), + ], + cwd=project, + check=True, + capture_output=True, + ) + (wheel,) = wheels.glob("*.whl") + with zipfile.ZipFile(wheel) as archive: + shipped = { + name for name in archive.namelist() + if name.startswith("study_schema/linkml/") and name.endswith(".yaml") + } + expected = { + f"study_schema/linkml/{path.relative_to(LINKML).as_posix()}" + for path in LINKML.rglob("*.yaml") + } + assert expected + assert shipped == expected + assert "study_schema/linkml/extraction-deviations.yaml" in shipped + assert "study_schema/linkml/extraction-to-storage.map.yaml" in shipped diff --git a/tests/test_statistics.py b/tests/test_statistics.py index 04e6028..1d220f9 100644 --- a/tests/test_statistics.py +++ b/tests/test_statistics.py @@ -1,7 +1,6 @@ -from pathlib import Path - import yaml +from study_schema import linkml from study_schema.statistics import ( DIRECTIONAL_KINDS, NON_DIRECTIONAL_KINDS, @@ -9,7 +8,7 @@ point_side, ) -ENUMS = Path(__file__).parent.parent / "neuroimaging-paper-parse" / "parse_enums.yaml" +ENUMS = linkml.EXTRACTION.parent / "neuroimaging-paper-parse" / "parse_enums.yaml" def test_kind_sets_match_the_schema_annotations(): diff --git a/tests/test_unique_keys.py b/tests/test_unique_keys.py index 81fa41e..cb930f0 100644 --- a/tests/test_unique_keys.py +++ b/tests/test_unique_keys.py @@ -6,7 +6,7 @@ # A golden check of the YAML, not a sync with neurostore: these pins are neurostore's # DECLARED_KEYS in neurostore.study_schema_tools on its review branch. Each key matches a -# unique_keys declaration in neuroimaging-study-storage/analysis.yaml, with its slots in +# unique_keys declaration in src/study_schema/linkml/neuroimaging-study-storage/analysis.yaml, with its slots in # declared order. NEUROSTORE_DECLARED_KEYS = { "Cell": (("term", "level"),), diff --git a/tools/generate_models.py b/tools/generate_models.py index a85a363..ce30a8f 100644 --- a/tools/generate_models.py +++ b/tools/generate_models.py @@ -17,6 +17,8 @@ from dataclasses import dataclass from pathlib import Path +from study_schema import linkml + ROOT = Path(__file__).resolve().parents[1] PACKAGE = ROOT / "src" / "study_schema" @@ -24,7 +26,7 @@ @dataclass(frozen=True) class Contract: module: str - schema: str + schema: Path #: JSON Schema file name -> top-level class. roots: dict[str, str] @@ -32,7 +34,7 @@ class Contract: CONTRACTS = ( Contract( module="paper_parse", - schema="neuroimaging-paper-parse.yaml", + schema=linkml.PAPER_PARSE, roots={ "parsed-paper": "ParsedPaper", "coordinate-parse": "CoordinateParse", @@ -40,12 +42,12 @@ class Contract: ), Contract( module="extraction", - schema="neuroimaging-study-extraction.yaml", + schema=linkml.EXTRACTION, roots={"extraction-record": "Study"}, ), Contract( module="storage", - schema="neuroimaging-study-storage.yaml", + schema=linkml.STORAGE, roots={"storage-study": "Study"}, ), ) @@ -53,7 +55,7 @@ class Contract: def _header(contract: Contract) -> str: return ( - f"# Generated by tools/generate_models.py from {contract.schema}.\n" + f"# Generated by tools/generate_models.py from {contract.schema.name}.\n" "# Do not edit: change the YAML and regenerate.\n" ) @@ -61,7 +63,7 @@ def _header(contract: Contract) -> str: def _view(contract: Contract): from linkml_runtime.utils.schemaview import SchemaView - view = SchemaView(str(ROOT / contract.schema)) + view = SchemaView(str(contract.schema)) view.merge_imports() return view