Skip to content

Linkml conversion tooling - #387

Draft
yarikoptic wants to merge 96 commits into
masterfrom
linkml-conversion
Draft

Linkml conversion tooling#387
yarikoptic wants to merge 96 commits into
masterfrom
linkml-conversion

Conversation

@yarikoptic

@yarikoptic yarikoptic commented Mar 20, 2026

Copy link
Copy Markdown
Member

This is an extract with amends from

which (branch linkml-auto-converted) would keep merging this branch into itself while reflecting on changes in the branch which could be rebased or gain merges from the master, and also can accumulate or drop "patch branches" from within its script defining what to patch with.

This way linkml-auto-converted would represent reflection of current state of conversion

TODO/PLAN

  • Establish branch linkml-auto-converted -- that one in WiP: Branch with auto converted linkml model #381
  • Made ‘hatch’ script (you could add pydantic2linkml as dependency there) to convert orig_models.py into dandischema/models.yaml : hatch ... TODO
  • Translated the original models.py into dandischema/models.yaml and overlaid with an [dandischema/models_overlay.yaml] overlay file.
  • script tools/linkml_conversion to convert into ‘linkml-auto-converted’
  • define model_instances.yaml (or alike) which would define pre-populated records such as standards (bids, nwb, ...). aim for potentially multiple classes there.
  • add a github workflow here which would react to changes into 'master' and this branch and with manual dispatch, which would first merge master into this branch, then run the script, and push results to linkml-auto-converted branch. This way we would always have 'up to date' and automatically updated state of that branch.
  • address "notes" about failed conversions one way (changing current dandi-schema pydantic model) or another (pydantic2linkml) or !
    • we can add a custom script to "enhance" auto generate linkml model to address any changes needed programmatically!
    • we can have a branch (or just a .patch file) with changes to perform on top of converted linkml
  • ...
  • There you produce pydantic model out of this patched model sufficient (although potentially more relaxed) to replace current pydantic model.

candleindark and others added 17 commits March 13, 2026 17:01
 Specify Hatch-managed env for auto converting
 `dandischema.models` to LinkML schema and
 back to Pydantic models
Provide script to translate `dandischema.models`
in to a LinkML schema and overly it with
definition provided by an overlay file.
Provide script to translate `dandischema/models.yaml`
back to Pydantic models and store them in
`dandischema/models.py`
The previous BRE pattern used `\+` (GNU sed extension) which silently
fails
on macOS BSD sed. Switch to `-E` (extended regex) with POSIX character
class
`[^[:space:]]` instead of `\S` (also unsupported by BSD sed), making the
normalization work on both macOS and Linux.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

Expand comment for linkml-auto-converted hatch env with usage instructions

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
There is no prefix defined as `dandi_default`.
The intended default prefix is `dandi`
…ed and some symbols from _orig for now

we do it so it does not overlay models.py since then git
is unable to track renames
we had to maintain original filename for models.py to apply patches
easily
Comment thread tools/linkml_conversion Outdated
# Poor man patch queue implementation
# Edit this list if you want to merge or drop PRs branches to be patched with.
# Order matters
branches_to_merge=( remove-discriminated-unions )

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that is where we define branches from PRs to merge!

@codecov

codecov Bot commented Mar 20, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 0% with 4 lines in your changes missing coverage. Please review.
✅ Project coverage is 48.66%. Comparing base (510f161) to head (36393de).

Files with missing lines Patch % Lines
dandischema/models_importstab.py 0.00% 4 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master     #387      +/-   ##
==========================================
+ Coverage   47.96%   48.66%   +0.70%     
==========================================
  Files          19       20       +1     
  Lines        2427     2431       +4     
==========================================
+ Hits         1164     1183      +19     
+ Misses       1263     1248      -15     
Flag Coverage Δ
unittests 48.66% <0.00%> (+0.70%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@candleindark
candleindark force-pushed the linkml-conversion branch 2 times, most recently from 59b0587 to c0fbd02 Compare March 31, 2026 00:59
…nator

`dandischema.models` use `schemaKey` in each
Pydantic as a de facto type designator in
LinkML. However, director translation
to LinkML based on individual model's
defintion is not possible. This override
provided in the merge file completes the
translation
Comment thread pyproject.toml Outdated
candleindark and others added 3 commits May 15, 2026 11:31
Add LinkML behavior tests for required: False -> True slot_usage refinement
Collection is supposed to be confined to `dandischema/` via the literal
positional arg in `commands = pytest -v {posargs} dandischema`. Under
tox 4.25 (local) that works as intended. Under tox 4.54 (CI), however,
the literal `dandischema` following `{posargs}` is dropped from the
invocation, leaving pytest to default to rootdir-based collection. That
made it walk into `tests/linkml_behavior/required_refinement/`, whose
dependencies (PyYAML, linkml, etc.) live in the dedicated
`linkml-behavior-test` hatch env rather than the tox env, and fail
collection with `ModuleNotFoundError: No module named 'yaml'`.

Set `testpaths = dandischema` in the `[pytest]` section so pytest's
default-collection target matches the intended scope of this test
suite. With that in place, the literal `dandischema` in the tox command
is redundant — and was the source of the tox-version-dependent
behavior — so drop it. A path explicitly supplied via `{posargs}` (e.g.
`tox -e py -- tests/foo.py`) still overrides `testpaths` and is now
honored cleanly instead of being combined with `dandischema`.

Co-Authored-By: Claude Code 2.1.143 / Claude Opus claude-opus-4-7 <noreply@anthropic.com>
Resolve conflit in `tox.ini` regarding
pytest settings
candleindark and others added 26 commits May 28, 2026 15:11
Introduce `docs/designs/migration_to_linkml_playbook/` as a living, self-updating
playbook for the ongoing migration of `dandischema` from Pydantic-defined models
to a LinkML-defined schema as the source of truth.

The foundation covers the problem statement and success criteria, the current
wiring of `./tools/linkml_conversion` (including the `2linkml` / `2pydantic` /
`2json` / `pydantic2json` Hatch scripts and the role of each file under
`tools/linkml_conversion_tools/`), a repeatable procedure with parity checks
against the dandi-archive frontend, an inventory of the patch queue applied
during translation (`master`, `remove-discriminated-unions`), and conventions
for the `log.md` / `findings.md` / `tools/` / `context/` subfiles. More content
will be added in subsequent commits.

Co-Authored-By: Claude Code 2.1.154 / Claude Opus 4.7 <noreply@anthropic.com>
Introduce `context/roles/` in the playbook, with a mandatory
`senior-developer.md` baseline (behavioral habits inherited by every
agent and subagent) plus topical stubs for Vue, Django, and LinkML
slices of the migration. Role files are loaded into a working agent's
context when a session touches the corresponding slice; the same files
also serve as spawn-prompt material when a subagent is invoked.

Each topical stub names its scope, its explicit not-in-scope handoffs to
sibling roles, and curated references for filling in its content
(including first-party LinkML AGENTS.md / SKILL.md material in
`linkml/linkml`, the LinkML specification at w3id.org, and community
subagent collections vetted as starting material). OVERVIEW.md gains a
pointer to `context/roles/` from the "How to use this directory"
section.

Co-Authored-By: Claude Code 2.1.154 / Claude Opus 4.7 <noreply@anthropic.com>
Fill in the "What this role needs to know" sections of `vue.md`,
`django.md`, and `linkml.md` from primary sources: the local
`dandi-archive` (frontend `package.json`, backend `pyproject.toml`, dev
docs) and `pydantic2linkml` (README, CLAUDE.md, source). Each role names
its stack landscape, the seam where dandischema crosses into that slice,
and a curated lift from upstream community subagent definitions with
caveats about version mismatches.

Shared findings across roles surfaced:

- @koumoul/vjsf is the form generator that consumes the JSON Schema in
  the dandi-archive frontend — the concrete consumer for criterion 3.
- dandi-archive pins dandischema==0.12.1 exact; dandi-cli pins
  dandischema ~= 0.12.0 — the generated Pydantic must satisfy both.
- `pydantic2linkml -M` is implemented via deepmerge.always_merger; the
  README's "values from the file win on conflict" oversimplifies. The
  actual per-type rule: dicts deep-merge, lists append, sets union,
  type-mismatches and scalars override. OVERVIEW.md's wiring and
  procedure step 7 are corrected to match, and a list-replacement
  escape hatch is documented (use `-O` for top-level lists, or fix on
  the Pydantic side).

Co-Authored-By: Claude Code 2.1.154 / Claude Opus 4.7 <noreply@anthropic.com>
Add a self-contained, runnable exhibit under the playbook's tools/
directory demonstrating that LinkML's `designates_type: true` makes the
generated Pydantic models and JSON Schema resolve a superclass-typed slot
value to its concrete subtype, and pins each class's `schemaKey` to its
class name consistently across both representations.

The exhibit is organized into:
- schemas/ — two source schemas (with/without the type designator) plus
  their generated Pydantic + JSON Schema snapshots
- subtype_resolution/ — demos asserting a Project survives in a
  BareAsset.wasGeneratedBy list only with the designator on
- schemakey_validation/ — demos asserting schemaKey validation
  (valid/absent/null/wrong-class) agrees between Pydantic and JSON Schema

Record the finding in findings.md. Demos run in the linkml-auto-converted
pipeline env (linkml 1.10.0). Exclude each exhibit's schemas/ folder from
pre-commit, since the generated snapshots are raw gen-pydantic /
gen-json-schema output that the linters should not touch.

Co-Authored-By: Claude Code 2.1.159 / Claude Opus 4.8 <noreply@anthropic.com>
The linkml-auto-converted pipeline env now pins linkml==1.11.1 (was
1.10.0). Regenerate the demo's Pydantic/JSON Schema snapshots under 1.11.1
and update the exhibit README + findings.md provenance accordingly. The
finding is unchanged: the designates_type behavior is identical across both
versions, and all four demos still pass.

Co-Authored-By: Claude Code 2.1.159 / Claude Opus 4.8 <noreply@anthropic.com>
LinkML's gen-json-schema hardcodes draft 2019-09; Pydantic and the
dandi-archive frontend's Ajv are both on 2020-12. The frontend deletes
$schema before validating, so the gap is mostly cosmetic except for
tuple arrays. Promote this into findings.md and partly answer the
matching open question in OVERVIEW.md.

Co-Authored-By: Claude Code 2.1.159 / Claude Opus 4.8 claude-opus-4-8 <noreply@anthropic.com>
…forces them

Verified against the linkml-auto-converted pipeline env (linkml 1.11.1): a
LinkML class rule (precondition value_presence PRESENT -> postcondition
required/minimum_value) is translated to an if/then block by gen-json-schema
and enforced, but gen-pydantic stores it only as inert linkml_meta metadata
and the generated model accepts violating instances. Also notes the
expressiveness limit: nested-slot and dict-key checks are not cleanly
expressible via rule slot_conditions.

Relevant to preserving dandischema's conditional (publish-only) validators
across the migration without losing Pydantic-side enforcement.

Co-Authored-By: Claude Code 2.1.161 / Claude Opus 4.8 claude-opus-4-8 <noreply@anthropic.com>
`ifabsent: null` had no effect: the `-M` deep merge inserts it as
`ifabsent: None`, then `canonicalize_schema_yml` drops it as unset, so
`models.yaml` is identical either way. The slot needs no `ifabsent`
anyway, since `designates_type: true` already supplies the `schemaKey`
default.

Co-Authored-By: Claude Code 2.1.218 / Opus 4.8 (claude-opus-4-8) <noreply@anthropic.com>
The range of this property can't be translated
by pydantic2linkml systematically because
it is not a listed an allowed monotonic
refinement defined within pydantic2linkml. This commit
specifies the range of
`Dandiset.wasGeneratedBy` through the
`models_merge.yaml`
The range of this property can't be translated
by pydantic2linkml systematically because
it is not a listed an allowed monotonic
refinement defined within pydantic2linkml. This commit
specifies the range of
`BareAsset.wasGeneratedBy` through the
`models_merge.yaml`
`remove_notes_by_pattern.py` matched its patterns against every `notes`
entry, so a pattern could not target one location without suppressing
identical notes elsewhere.

Replace the flat `PATTERNS` list with `REMOVALS`, pairing each regex with
an optional tuple of paths. A path ending in `"notes"` designates that
node alone; any other path is a subtree root. A rule without paths keeps
the previous document-wide behavior, so the existing patterns are
unaffected and the output is unchanged.

Co-Authored-By: Claude Code 2.1.220 / Claude Opus 5 (claude-opus-5) <noreply@anthropic.com>
…ages

The `range` of `BareAsset.slot_usage.wasGeneratedBy` and
`Dandiset.slot_usage.wasGeneratedBy` is now supplied through
`dandischema/models_merge.yaml`, so the note `pydantic2linkml` emits
about being unable to express it is obsolete.

Scope the removal to those two locations.

Co-Authored-By: Claude Code 2.1.220 / Claude Opus 5 (claude-opus-5) <noreply@anthropic.com>
Adding a second topic directory under `tests/linkml_behavior/` collides
with this one. Both pytest's default "prepend" import mode and `mypy`
resolve the identically named modules in sibling topics (`_cases`,
`test_validate`, ...) to the same top-level module name, and refuse to
collect the second one.

Add an `__init__.py` so those modules are namespaced by their topic, and
import the shared case lists relatively to match.

Co-Authored-By: Claude Code 2.1.220 / Claude Opus 5 (claude-opus-5) <noreply@anthropic.com>
…rides

`dandischema/models_merge.yaml` respecifies the range of the inherited
multivalued `wasGeneratedBy` slot through `slot_usage`, narrowing it to a
subclass for `Dandiset` and widening it to `Any` plus an `any_of` for
`BareAsset`. Add a `range_refinement` topic exercising both forms of
respecification against all three validators the pipeline uses.

Also record in `findings.md` that the `BareAsset` override is redundant,
since `designates_type: true` already expands `range: Activity` over its
descendants, along with the intent behind keeping it.

Co-Authored-By: Claude Code 2.1.220 / Claude Opus 5 (claude-opus-5) <noreply@anthropic.com>
…ftests

Both LinkML-behavior topics generated their JSON schemas, JSON instances
and Pydantic models with byte-identical fixture bodies, differing only in
the schema, class and instance names they fed in.

Move that generation into `tests/linkml_behavior/_generation.py`, leaving
each `conftest.py` as its own data plus thin fixtures delegating to it.

The fixtures stay topic-local rather than moving to a parent `conftest.py`,
which would share one `FixtureDef` across topics and, at session scope,
hand the first topic's artifacts to every later one.

Co-Authored-By: Claude Code 2.1.220 / Claude Opus 5 (claude-opus-5) <noreply@anthropic.com>
…modules

The `test_*.py` modules were identical across topics apart from their
docstrings, each repeating the same subprocess invocation and assertion
message.

Move those into `tests/linkml_behavior/_validators.py`, one `assert_*`
helper per validator, leaving each test module its docstring, case lists
and `@pytest.mark.parametrize` decorators.

Co-Authored-By: Claude Code 2.1.220 / Claude Opus 5 (claude-opus-5) <noreply@anthropic.com>
The playbook described these as JSON Schemas from the original Pydantic
models, kept as the parity baseline. They are not: `linkml_conversion`
runs `pydantic2json` after the rename dance, so `publish_model_schemata`
resolves `dandischema.models` to the import stub and serializes the
generated models instead. The script itself is neutral about which models
it serializes; the ordering decides.

Correct the three affected claims, point the parity baseline at the
schemata published in `dandi/schema`, and record the correction in
`log.md`.

Co-Authored-By: Claude Code 2.1.220 / Claude Opus 5 (claude-opus-5) <noreply@anthropic.com>
The notes are no longer necessary since #419.

Also handle in `find_schemakey_mismatches.py` the re-exposure of a class under
an alias.

Addresses the `MANUAL_NOTE` sub-task of #389.

Co-Authored-By: Claude Code 2.1.224 / Claude Opus 5 <noreply@anthropic.com>
`models.yaml` no longer defines them since #419 consolidated them into
`Dandiset` and `Asset`, so `gen-json-schema` emitted rootless schemas that
accept any object. The filename `sed` existed only for those two targets and
goes with them.

Co-Authored-By: Claude Code 2.1.224 / Claude Opus 5 <noreply@anthropic.com>
`publish_model_schemata` resolves `SCHEMA_MAP` keys with `getattr`, so the stub
needs the aliases #419 kept in `dandischema/models.py`. Temporary: remove them
after the #419 follow-up that drops those aliases.

Co-Authored-By: Claude Code 2.1.224 / Claude Opus 5 <noreply@anthropic.com>
`2json` no longer targets them, and the log entry in 09730f8 missed that the
phantom classes were load-bearing for `2json` and `publish_model_schemata`.

Co-Authored-By: Claude Code 2.1.224 / Claude Opus 5 <noreply@anthropic.com>
The finding attributed both halves of the slot usage to
`dandischema/models_merge.yaml`. That file supplies only `range: Any`;
`pydantic2linkml` emits the `any_of` from the Pydantic annotation. The
finding's conclusion is unaffected.

Co-Authored-By: Claude Code 2.1.223 / claude-opus-5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants