oold 2.2.0__tar.gz → 2.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (282) hide show
  1. {oold-2.2.0 → oold-2.3.0}/CHANGELOG.md +25 -0
  2. {oold-2.2.0 → oold-2.3.0}/CITATION.cff +1 -1
  3. {oold-2.2.0 → oold-2.3.0}/PKG-INFO +1 -1
  4. {oold-2.2.0 → oold-2.3.0}/docs/how-to/controller.md +27 -1
  5. {oold-2.2.0 → oold-2.3.0}/pyproject.toml +1 -1
  6. {oold-2.2.0 → oold-2.3.0}/src/oold/backend/auth.py +10 -0
  7. {oold-2.2.0 → oold-2.3.0}/src/oold/model/__init__.py +185 -13
  8. {oold-2.2.0 → oold-2.3.0}/src/oold/model/_compat.py +4 -2
  9. {oold-2.2.0 → oold-2.3.0}/src/oold/model/_descriptor.py +60 -12
  10. {oold-2.2.0 → oold-2.3.0}/src/oold/model/v1/__init__.py +129 -8
  11. {oold-2.2.0 → oold-2.3.0}/src/oold/model/v1/_descriptor.py +11 -6
  12. {oold-2.2.0 → oold-2.3.0}/src/oold/static.py +50 -22
  13. {oold-2.2.0 → oold-2.3.0}/tests/conftest.py +19 -4
  14. {oold-2.2.0 → oold-2.3.0}/tests/test_auth.py +16 -0
  15. oold-2.3.0/tests/test_register_controller_type.py +114 -0
  16. oold-2.3.0/tests/test_register_controller_type_v1.py +96 -0
  17. oold-2.3.0/tests/test_registry_precedence.py +302 -0
  18. {oold-2.2.0 → oold-2.3.0}/uv.lock +1 -1
  19. {oold-2.2.0 → oold-2.3.0}/.devcontainer/devcontainer.json +0 -0
  20. {oold-2.2.0 → oold-2.3.0}/.devcontainer/postCreateCommand.sh +0 -0
  21. {oold-2.2.0 → oold-2.3.0}/.editorconfig +0 -0
  22. {oold-2.2.0 → oold-2.3.0}/.gitattributes +0 -0
  23. {oold-2.2.0 → oold-2.3.0}/.github/ISSUE_TEMPLATE.md +0 -0
  24. {oold-2.2.0 → oold-2.3.0}/.github/actions/setup-python-env/action.yml +0 -0
  25. {oold-2.2.0 → oold-2.3.0}/.github/workflows/main.yml +0 -0
  26. {oold-2.2.0 → oold-2.3.0}/.github/workflows/on-release-main.yml +0 -0
  27. {oold-2.2.0 → oold-2.3.0}/.github/workflows/validate-codecov-config.yml +0 -0
  28. {oold-2.2.0 → oold-2.3.0}/.gitignore +0 -0
  29. {oold-2.2.0 → oold-2.3.0}/.mcp.json +0 -0
  30. {oold-2.2.0 → oold-2.3.0}/.pre-commit-config.yaml +0 -0
  31. {oold-2.2.0 → oold-2.3.0}/CLAUDE.md +0 -0
  32. {oold-2.2.0 → oold-2.3.0}/CONTRIBUTING.md +0 -0
  33. {oold-2.2.0 → oold-2.3.0}/Dockerfile +0 -0
  34. {oold-2.2.0 → oold-2.3.0}/LICENSE +0 -0
  35. {oold-2.2.0 → oold-2.3.0}/Makefile +0 -0
  36. {oold-2.2.0 → oold-2.3.0}/README.md +0 -0
  37. {oold-2.2.0 → oold-2.3.0}/codecov.yaml +0 -0
  38. {oold-2.2.0 → oold-2.3.0}/docs/about.md +0 -0
  39. {oold-2.2.0 → oold-2.3.0}/docs/api.md +0 -0
  40. {oold-2.2.0 → oold-2.3.0}/docs/architecture.md +0 -0
  41. {oold-2.2.0 → oold-2.3.0}/docs/assets/oold_concept.png +0 -0
  42. {oold-2.2.0 → oold-2.3.0}/docs/contributing.md +0 -0
  43. {oold-2.2.0 → oold-2.3.0}/docs/design/downstream-migration.md +0 -0
  44. {oold-2.2.0 → oold-2.3.0}/docs/design/graph-object-binding.md +0 -0
  45. {oold-2.2.0 → oold-2.3.0}/docs/get-started.md +0 -0
  46. {oold-2.2.0 → oold-2.3.0}/docs/how-to/backends.md +0 -0
  47. {oold-2.2.0 → oold-2.3.0}/docs/how-to/codegen.md +0 -0
  48. {oold-2.2.0 → oold-2.3.0}/docs/how-to/index.md +0 -0
  49. {oold-2.2.0 → oold-2.3.0}/docs/how-to/object-graph-mapping.md +0 -0
  50. {oold-2.2.0 → oold-2.3.0}/docs/how-to/rdf-export.md +0 -0
  51. {oold-2.2.0 → oold-2.3.0}/docs/how-to/validation.md +0 -0
  52. {oold-2.2.0 → oold-2.3.0}/docs/index.md +0 -0
  53. {oold-2.2.0 → oold-2.3.0}/docs/maintaining-meta-schemas.md +0 -0
  54. {oold-2.2.0 → oold-2.3.0}/docs/stylesheets/extra.css +0 -0
  55. {oold-2.2.0 → oold-2.3.0}/examples/backend_auth.py +0 -0
  56. {oold-2.2.0 → oold-2.3.0}/examples/bench_attribute_access.py +0 -0
  57. {oold-2.2.0 → oold-2.3.0}/examples/bench_binding_variants.py +0 -0
  58. {oold-2.2.0 → oold-2.3.0}/examples/check_binding_features.py +0 -0
  59. {oold-2.2.0 → oold-2.3.0}/examples/linked_data_editor.ipynb +0 -0
  60. {oold-2.2.0 → oold-2.3.0}/examples/linked_data_editor.py +0 -0
  61. {oold-2.2.0 → oold-2.3.0}/examples/notation_example.py +0 -0
  62. {oold-2.2.0 → oold-2.3.0}/examples/wiki_data.py +0 -0
  63. {oold-2.2.0 → oold-2.3.0}/pytest.benchmark.ini +0 -0
  64. {oold-2.2.0 → oold-2.3.0}/scripts/check_markdown_extensions.py +0 -0
  65. {oold-2.2.0 → oold-2.3.0}/scripts/compare_benchmarks.py +0 -0
  66. {oold-2.2.0 → oold-2.3.0}/src/__init__.py +0 -0
  67. {oold-2.2.0 → oold-2.3.0}/src/oold/__init__.py +0 -0
  68. {oold-2.2.0 → oold-2.3.0}/src/oold/backend/__init__.py +0 -0
  69. {oold-2.2.0 → oold-2.3.0}/src/oold/backend/document_store.py +0 -0
  70. {oold-2.2.0 → oold-2.3.0}/src/oold/backend/interface.py +0 -0
  71. {oold-2.2.0 → oold-2.3.0}/src/oold/backend/sparql.py +0 -0
  72. {oold-2.2.0 → oold-2.3.0}/src/oold/cli.py +0 -0
  73. {oold-2.2.0 → oold-2.3.0}/src/oold/drafts/field_extras.py +0 -0
  74. {oold-2.2.0 → oold-2.3.0}/src/oold/drafts/ldc_generator.py +0 -0
  75. {oold-2.2.0 → oold-2.3.0}/src/oold/drafts/magic_field.py +0 -0
  76. {oold-2.2.0 → oold-2.3.0}/src/oold/experimental/__init__.py +0 -0
  77. {oold-2.2.0 → oold-2.3.0}/src/oold/experimental/codegen_spike.py +0 -0
  78. {oold-2.2.0 → oold-2.3.0}/src/oold/generator.py +0 -0
  79. {oold-2.2.0 → oold-2.3.0}/src/oold/model/_notation.py +0 -0
  80. {oold-2.2.0 → oold-2.3.0}/src/oold/model/_ref.py +0 -0
  81. {oold-2.2.0 → oold-2.3.0}/src/oold/ui/__init__.py +0 -0
  82. {oold-2.2.0 → oold-2.3.0}/src/oold/ui/anywidget/JsonEditorWidget.ipynb +0 -0
  83. {oold-2.2.0 → oold-2.3.0}/src/oold/ui/anywidget/__init__.py +0 -0
  84. {oold-2.2.0 → oold-2.3.0}/src/oold/ui/anywidget/jsoneditor.py +0 -0
  85. {oold-2.2.0 → oold-2.3.0}/src/oold/ui/nicegui/__init__.py +0 -0
  86. {oold-2.2.0 → oold-2.3.0}/src/oold/ui/nicegui/jsoneditor.py +0 -0
  87. {oold-2.2.0 → oold-2.3.0}/src/oold/ui/nicegui/main.py +0 -0
  88. {oold-2.2.0 → oold-2.3.0}/src/oold/ui/panel/__init__.py +0 -0
  89. {oold-2.2.0 → oold-2.3.0}/src/oold/ui/panel/anywidget_vite/jsoneditor.py +0 -0
  90. {oold-2.2.0 → oold-2.3.0}/src/oold/ui/panel/demo.py +0 -0
  91. {oold-2.2.0 → oold-2.3.0}/src/oold/ui/vue/README.md +0 -0
  92. {oold-2.2.0 → oold-2.3.0}/src/oold/ui/vue/package-lock.json +0 -0
  93. {oold-2.2.0 → oold-2.3.0}/src/oold/ui/vue/package.json +0 -0
  94. {oold-2.2.0 → oold-2.3.0}/src/oold/ui/vue/src/jsoneditor.vue +0 -0
  95. {oold-2.2.0 → oold-2.3.0}/src/oold/ui/vue/src/jsoneditor_component.js +0 -0
  96. {oold-2.2.0 → oold-2.3.0}/src/oold/ui/vue/src/jsoneditor_component.less +0 -0
  97. {oold-2.2.0 → oold-2.3.0}/src/oold/ui/vue/vite.config.anywidget.mjs +0 -0
  98. {oold-2.2.0 → oold-2.3.0}/src/oold/ui/vue/vite.config.js +0 -0
  99. {oold-2.2.0 → oold-2.3.0}/src/oold/ui/vue/vite.config.nicegui.js +0 -0
  100. {oold-2.2.0 → oold-2.3.0}/src/oold/utils/codegen.py +0 -0
  101. {oold-2.2.0 → oold-2.3.0}/src/oold/utils/environment.py +0 -0
  102. {oold-2.2.0 → oold-2.3.0}/src/oold/utils/json_tools.py +0 -0
  103. {oold-2.2.0 → oold-2.3.0}/src/oold/utils/transform.py +0 -0
  104. {oold-2.2.0 → oold-2.3.0}/src/oold/validation/__init__.py +0 -0
  105. {oold-2.2.0 → oold-2.3.0}/src/oold/validation/check_registry.py +0 -0
  106. {oold-2.2.0 → oold-2.3.0}/src/oold/validation/cli.py +0 -0
  107. {oold-2.2.0 → oold-2.3.0}/src/oold/validation/compliance.py +0 -0
  108. {oold-2.2.0 → oold-2.3.0}/src/oold/validation/context_graph.py +0 -0
  109. {oold-2.2.0 → oold-2.3.0}/src/oold/validation/context_resolution.py +0 -0
  110. {oold-2.2.0 → oold-2.3.0}/src/oold/validation/formats.py +0 -0
  111. {oold-2.2.0 → oold-2.3.0}/src/oold/validation/frame.py +0 -0
  112. {oold-2.2.0 → oold-2.3.0}/src/oold/validation/generate.py +0 -0
  113. {oold-2.2.0 → oold-2.3.0}/src/oold/validation/instance_checks.py +0 -0
  114. {oold-2.2.0 → oold-2.3.0}/src/oold/validation/loader.py +0 -0
  115. {oold-2.2.0 → oold-2.3.0}/src/oold/validation/mcp_server.py +0 -0
  116. {oold-2.2.0 → oold-2.3.0}/src/oold/validation/meta/0.7.0/oold-meta-schema.json +0 -0
  117. {oold-2.2.0 → oold-2.3.0}/src/oold/validation/meta/0.7.0/oold-pattern-lint.schema.json +0 -0
  118. {oold-2.2.0 → oold-2.3.0}/src/oold/validation/meta/0.7.0/oold-ui-meta-schema.json +0 -0
  119. {oold-2.2.0 → oold-2.3.0}/src/oold/validation/meta/0.8.0/oold-meta-schema.json +0 -0
  120. {oold-2.2.0 → oold-2.3.0}/src/oold/validation/meta/0.8.0/oold-pattern-lint.schema.json +0 -0
  121. {oold-2.2.0 → oold-2.3.0}/src/oold/validation/meta/0.8.0/oold-ui-meta-schema.json +0 -0
  122. {oold-2.2.0 → oold-2.3.0}/src/oold/validation/meta/1.0.0/oold-meta-schema-base.json +0 -0
  123. {oold-2.2.0 → oold-2.3.0}/src/oold/validation/meta/1.0.0/oold-meta-schema.json +0 -0
  124. {oold-2.2.0 → oold-2.3.0}/src/oold/validation/meta/1.0.0/oold-pattern-lint.schema.json +0 -0
  125. {oold-2.2.0 → oold-2.3.0}/src/oold/validation/meta/1.0.0/oold-rules.json +0 -0
  126. {oold-2.2.0 → oold-2.3.0}/src/oold/validation/meta/1.0.0/oold-rules.schema.json +0 -0
  127. {oold-2.2.0 → oold-2.3.0}/src/oold/validation/meta/1.0.0/oold-ui-meta-schema.json +0 -0
  128. {oold-2.2.0 → oold-2.3.0}/src/oold/validation/meta/1.0.0-rc.1/oold-meta-schema.json +0 -0
  129. {oold-2.2.0 → oold-2.3.0}/src/oold/validation/meta/1.0.0-rc.1/oold-pattern-lint.schema.json +0 -0
  130. {oold-2.2.0 → oold-2.3.0}/src/oold/validation/meta/1.0.0-rc.1/oold-rules.json +0 -0
  131. {oold-2.2.0 → oold-2.3.0}/src/oold/validation/meta/1.0.0-rc.1/oold-rules.schema.json +0 -0
  132. {oold-2.2.0 → oold-2.3.0}/src/oold/validation/meta/1.0.0-rc.1/oold-ui-meta-schema.json +0 -0
  133. {oold-2.2.0 → oold-2.3.0}/src/oold/validation/meta/1.0.0-rc.2/oold-meta-schema-base.json +0 -0
  134. {oold-2.2.0 → oold-2.3.0}/src/oold/validation/meta/1.0.0-rc.2/oold-meta-schema.json +0 -0
  135. {oold-2.2.0 → oold-2.3.0}/src/oold/validation/meta/1.0.0-rc.2/oold-pattern-lint.schema.json +0 -0
  136. {oold-2.2.0 → oold-2.3.0}/src/oold/validation/meta/1.0.0-rc.2/oold-rules.json +0 -0
  137. {oold-2.2.0 → oold-2.3.0}/src/oold/validation/meta/1.0.0-rc.2/oold-rules.schema.json +0 -0
  138. {oold-2.2.0 → oold-2.3.0}/src/oold/validation/meta/1.0.0-rc.2/oold-ui-meta-schema.json +0 -0
  139. {oold-2.2.0 → oold-2.3.0}/src/oold/validation/meta/1.0.0-rc.3/oold-meta-schema-base.json +0 -0
  140. {oold-2.2.0 → oold-2.3.0}/src/oold/validation/meta/1.0.0-rc.3/oold-meta-schema.json +0 -0
  141. {oold-2.2.0 → oold-2.3.0}/src/oold/validation/meta/1.0.0-rc.3/oold-pattern-lint.schema.json +0 -0
  142. {oold-2.2.0 → oold-2.3.0}/src/oold/validation/meta/1.0.0-rc.3/oold-rules.json +0 -0
  143. {oold-2.2.0 → oold-2.3.0}/src/oold/validation/meta/1.0.0-rc.3/oold-rules.schema.json +0 -0
  144. {oold-2.2.0 → oold-2.3.0}/src/oold/validation/meta/1.0.0-rc.3/oold-ui-meta-schema.json +0 -0
  145. {oold-2.2.0 → oold-2.3.0}/src/oold/validation/meta/1.0.0-rc.4/oold-meta-schema-base.json +0 -0
  146. {oold-2.2.0 → oold-2.3.0}/src/oold/validation/meta/1.0.0-rc.4/oold-meta-schema.json +0 -0
  147. {oold-2.2.0 → oold-2.3.0}/src/oold/validation/meta/1.0.0-rc.4/oold-pattern-lint.schema.json +0 -0
  148. {oold-2.2.0 → oold-2.3.0}/src/oold/validation/meta/1.0.0-rc.4/oold-rules.json +0 -0
  149. {oold-2.2.0 → oold-2.3.0}/src/oold/validation/meta/1.0.0-rc.4/oold-rules.schema.json +0 -0
  150. {oold-2.2.0 → oold-2.3.0}/src/oold/validation/meta/1.0.0-rc.4/oold-ui-meta-schema.json +0 -0
  151. {oold-2.2.0 → oold-2.3.0}/src/oold/validation/meta/1.0.0-rc.5/oold-meta-schema-base.json +0 -0
  152. {oold-2.2.0 → oold-2.3.0}/src/oold/validation/meta/1.0.0-rc.5/oold-meta-schema.json +0 -0
  153. {oold-2.2.0 → oold-2.3.0}/src/oold/validation/meta/1.0.0-rc.5/oold-pattern-lint.schema.json +0 -0
  154. {oold-2.2.0 → oold-2.3.0}/src/oold/validation/meta/1.0.0-rc.5/oold-rules.json +0 -0
  155. {oold-2.2.0 → oold-2.3.0}/src/oold/validation/meta/1.0.0-rc.5/oold-rules.schema.json +0 -0
  156. {oold-2.2.0 → oold-2.3.0}/src/oold/validation/meta/1.0.0-rc.5/oold-ui-meta-schema.json +0 -0
  157. {oold-2.2.0 → oold-2.3.0}/src/oold/validation/meta/index.json +0 -0
  158. {oold-2.2.0 → oold-2.3.0}/src/oold/validation/meta_store.py +0 -0
  159. {oold-2.2.0 → oold-2.3.0}/src/oold/validation/meta_vendor.py +0 -0
  160. {oold-2.2.0 → oold-2.3.0}/src/oold/validation/pattern_lint.py +0 -0
  161. {oold-2.2.0 → oold-2.3.0}/src/oold/validation/pipeline.py +0 -0
  162. {oold-2.2.0 → oold-2.3.0}/src/oold/validation/predicates.py +0 -0
  163. {oold-2.2.0 → oold-2.3.0}/src/oold/validation/report.py +0 -0
  164. {oold-2.2.0 → oold-2.3.0}/src/oold/validation/resolve.py +0 -0
  165. {oold-2.2.0 → oold-2.3.0}/src/oold/validation/roundtrip.py +0 -0
  166. {oold-2.2.0 → oold-2.3.0}/src/oold/validation/schema_checks.py +0 -0
  167. {oold-2.2.0 → oold-2.3.0}/tests/data/__init__.py +0 -0
  168. {oold-2.2.0 → oold-2.3.0}/tests/data/class_hierarchy/__init__.py +0 -0
  169. {oold-2.2.0 → oold-2.3.0}/tests/data/class_hierarchy/model_v1.py +0 -0
  170. {oold-2.2.0 → oold-2.3.0}/tests/data/class_hierarchy/model_v2.py +0 -0
  171. {oold-2.2.0 → oold-2.3.0}/tests/data/enum_docstrings/__init__.py +0 -0
  172. {oold-2.2.0 → oold-2.3.0}/tests/data/enum_docstrings/model_v1.py +0 -0
  173. {oold-2.2.0 → oold-2.3.0}/tests/data/enum_docstrings/model_v2.py +0 -0
  174. {oold-2.2.0 → oold-2.3.0}/tests/data/format_parity.json +0 -0
  175. {oold-2.2.0 → oold-2.3.0}/tests/data/oneof_subschema/__init__.py +0 -0
  176. {oold-2.2.0 → oold-2.3.0}/tests/data/oneof_subschema/model_v1.py +0 -0
  177. {oold-2.2.0 → oold-2.3.0}/tests/data/oneof_subschema/model_v2.py +0 -0
  178. {oold-2.2.0 → oold-2.3.0}/tests/data/oold/Address.schema.json +0 -0
  179. {oold-2.2.0 → oold-2.3.0}/tests/data/oold/Contact.schema.json +0 -0
  180. {oold-2.2.0 → oold-2.3.0}/tests/data/oold/ContactSeparateKeys.schema.json +0 -0
  181. {oold-2.2.0 → oold-2.3.0}/tests/data/oold/Minimal.schema.json +0 -0
  182. {oold-2.2.0 → oold-2.3.0}/tests/data/oold/Organization.schema.json +0 -0
  183. {oold-2.2.0 → oold-2.3.0}/tests/data/oold/OwlOrganization.instance.json +0 -0
  184. {oold-2.2.0 → oold-2.3.0}/tests/data/oold/OwlOrganization.schema.json +0 -0
  185. {oold-2.2.0 → oold-2.3.0}/tests/data/oold/Person.schema.json +0 -0
  186. {oold-2.2.0 → oold-2.3.0}/tests/data/oold/PersonWithPet.instance.json +0 -0
  187. {oold-2.2.0 → oold-2.3.0}/tests/data/oold/PersonWithPet.schema.json +0 -0
  188. {oold-2.2.0 → oold-2.3.0}/tests/data/oold/Pet.schema.json +0 -0
  189. {oold-2.2.0 → oold-2.3.0}/tests/data/oold/RdfPerson.instance.json +0 -0
  190. {oold-2.2.0 → oold-2.3.0}/tests/data/oold/RdfPerson.schema.json +0 -0
  191. {oold-2.2.0 → oold-2.3.0}/tests/data/oold/Researcher.schema.json +0 -0
  192. {oold-2.2.0 → oold-2.3.0}/tests/data/oold/Thing.schema.json +0 -0
  193. {oold-2.2.0 → oold-2.3.0}/tests/data/oold/UiAnnotations.schema.json +0 -0
  194. {oold-2.2.0 → oold-2.3.0}/tests/data/oold/UiOverlay.json +0 -0
  195. {oold-2.2.0 → oold-2.3.0}/tests/data/oold/broken/Gauge.schema.json +0 -0
  196. {oold-2.2.0 → oold-2.3.0}/tests/data/oold/broken/NarrowBase.schema.json +0 -0
  197. {oold-2.2.0 → oold-2.3.0}/tests/data/oold/broken/Sensor.schema.json +0 -0
  198. {oold-2.2.0 → oold-2.3.0}/tests/data/oold/broken/array_without_container.schema.json +0 -0
  199. {oold-2.2.0 → oold-2.3.0}/tests/data/oold/broken/base_uri_misaligned.schema.json +0 -0
  200. {oold-2.2.0 → oold-2.3.0}/tests/data/oold/broken/branch_context_conflict.schema.json +0 -0
  201. {oold-2.2.0 → oold-2.3.0}/tests/data/oold/broken/closed_object_rejects_metadata.schema.json +0 -0
  202. {oold-2.2.0 → oold-2.3.0}/tests/data/oold/broken/context_array_order_mismatch.schema.json +0 -0
  203. {oold-2.2.0 → oold-2.3.0}/tests/data/oold/broken/inline_type_disagrees.schema.json +0 -0
  204. {oold-2.2.0 → oold-2.3.0}/tests/data/oold/broken/invalid_meta.schema.json +0 -0
  205. {oold-2.2.0 → oold-2.3.0}/tests/data/oold/broken/iri_reference_without_format.schema.json +0 -0
  206. {oold-2.2.0 → oold-2.3.0}/tests/data/oold/broken/legacy_dialect.schema.json +0 -0
  207. {oold-2.2.0 → oold-2.3.0}/tests/data/oold/broken/missing_context_term.schema.json +0 -0
  208. {oold-2.2.0 → oold-2.3.0}/tests/data/oold/broken/narrow_only_relaxation.schema.json +0 -0
  209. {oold-2.2.0 → oold-2.3.0}/tests/data/oold/broken/no_context.schema.json +0 -0
  210. {oold-2.2.0 → oold-2.3.0}/tests/data/oold/broken/root_ref_not_reflected.schema.json +0 -0
  211. {oold-2.2.0 → oold-2.3.0}/tests/data/oold/broken/undefined_prefix.schema.json +0 -0
  212. {oold-2.2.0 → oold-2.3.0}/tests/data/oold/broken/unresolvable_context_ref.schema.json +0 -0
  213. {oold-2.2.0 → oold-2.3.0}/tests/data/oold/broken/versioned_id_missing_version.schema.json +0 -0
  214. {oold-2.2.0 → oold-2.3.0}/tests/data/oold/broken/vocab_covers_the_remainder.schema.json +0 -0
  215. {oold-2.2.0 → oold-2.3.0}/tests/data/oold/broken/xsd_string_coercion.schema.json +0 -0
  216. {oold-2.2.0 → oold-2.3.0}/tests/data/oold/compliance/jsonld-features.json +0 -0
  217. {oold-2.2.0 → oold-2.3.0}/tests/data/oold/compliance/oold-vocab.json +0 -0
  218. {oold-2.2.0 → oold-2.3.0}/tests/data/oold/compliance/roundtrip-patterns.json +0 -0
  219. {oold-2.2.0 → oold-2.3.0}/tests/data/oold/remote_context/Leaf.schema.json +0 -0
  220. {oold-2.2.0 → oold-2.3.0}/tests/data/oold/x_oold_context/SkosSynonym.schema.json +0 -0
  221. {oold-2.2.0 → oold-2.3.0}/tests/data/quantities/__init__.py +0 -0
  222. {oold-2.2.0 → oold-2.3.0}/tests/data/quantities/model.py +0 -0
  223. {oold-2.2.0 → oold-2.3.0}/tests/data/resolver_cache/66a1009a2cf528dd525ec0c0150488fe.json +0 -0
  224. {oold-2.2.0 → oold-2.3.0}/tests/data/subclass_inheritance/__init__.py +0 -0
  225. {oold-2.2.0 → oold-2.3.0}/tests/data/subclass_inheritance/model_v1.py +0 -0
  226. {oold-2.2.0 → oold-2.3.0}/tests/data/subclass_inheritance/model_v2.py +0 -0
  227. {oold-2.2.0 → oold-2.3.0}/tests/data/test_core/__init__.py +0 -0
  228. {oold-2.2.0 → oold-2.3.0}/tests/data/test_core/model_v1.py +0 -0
  229. {oold-2.2.0 → oold-2.3.0}/tests/data/test_core/model_v1_nested.py +0 -0
  230. {oold-2.2.0 → oold-2.3.0}/tests/data/test_core/model_v2.py +0 -0
  231. {oold-2.2.0 → oold-2.3.0}/tests/data/test_core/model_v2_nested.py +0 -0
  232. {oold-2.2.0 → oold-2.3.0}/tests/data/test_core/src/Bar.json +0 -0
  233. {oold-2.2.0 → oold-2.3.0}/tests/data/test_core/src/Foo.json +0 -0
  234. {oold-2.2.0 → oold-2.3.0}/tests/data/test_core/src/bar2/Bar2.json +0 -0
  235. {oold-2.2.0 → oold-2.3.0}/tests/query_test.py +0 -0
  236. {oold-2.2.0 → oold-2.3.0}/tests/static.py +0 -0
  237. {oold-2.2.0 → oold-2.3.0}/tests/test_auto_descriptor_binding.py +0 -0
  238. {oold-2.2.0 → oold-2.3.0}/tests/test_backends.py +0 -0
  239. {oold-2.2.0 → oold-2.3.0}/tests/test_base_controller.py +0 -0
  240. {oold-2.2.0 → oold-2.3.0}/tests/test_binding_switch.py +0 -0
  241. {oold-2.2.0 → oold-2.3.0}/tests/test_code_generation.py +0 -0
  242. {oold-2.2.0 → oold-2.3.0}/tests/test_compat_parity.py +0 -0
  243. {oold-2.2.0 → oold-2.3.0}/tests/test_compat_parity_v1.py +0 -0
  244. {oold-2.2.0 → oold-2.3.0}/tests/test_downstream_shapes.py +0 -0
  245. {oold-2.2.0 → oold-2.3.0}/tests/test_link_annotation.py +0 -0
  246. {oold-2.2.0 → oold-2.3.0}/tests/test_notation.py +0 -0
  247. {oold-2.2.0 → oold-2.3.0}/tests/test_oold.py +0 -0
  248. {oold-2.2.0 → oold-2.3.0}/tests/test_rdf.py +0 -0
  249. {oold-2.2.0 → oold-2.3.0}/tests/test_ref.py +0 -0
  250. {oold-2.2.0 → oold-2.3.0}/tests/test_register_type.py +0 -0
  251. {oold-2.2.0 → oold-2.3.0}/tests/test_schema_generation.py +0 -0
  252. {oold-2.2.0 → oold-2.3.0}/tests/test_serialization.py +0 -0
  253. {oold-2.2.0 → oold-2.3.0}/tests/test_sparql_query.py +0 -0
  254. {oold-2.2.0 → oold-2.3.0}/tests/test_transform.py +0 -0
  255. {oold-2.2.0 → oold-2.3.0}/tests/test_typing.py +0 -0
  256. {oold-2.2.0 → oold-2.3.0}/tests/test_validation/__init__.py +0 -0
  257. {oold-2.2.0 → oold-2.3.0}/tests/test_validation/conftest.py +0 -0
  258. {oold-2.2.0 → oold-2.3.0}/tests/test_validation/test_check_registry.py +0 -0
  259. {oold-2.2.0 → oold-2.3.0}/tests/test_validation/test_check_registry_drift.py +0 -0
  260. {oold-2.2.0 → oold-2.3.0}/tests/test_validation/test_checks.py +0 -0
  261. {oold-2.2.0 → oold-2.3.0}/tests/test_validation/test_cli.py +0 -0
  262. {oold-2.2.0 → oold-2.3.0}/tests/test_validation/test_compliance.py +0 -0
  263. {oold-2.2.0 → oold-2.3.0}/tests/test_validation/test_emitted_schema_conformance.py +0 -0
  264. {oold-2.2.0 → oold-2.3.0}/tests/test_validation/test_formats.py +0 -0
  265. {oold-2.2.0 → oold-2.3.0}/tests/test_validation/test_generate.py +0 -0
  266. {oold-2.2.0 → oold-2.3.0}/tests/test_validation/test_jsonld.py +0 -0
  267. {oold-2.2.0 → oold-2.3.0}/tests/test_validation/test_mcp_server.py +0 -0
  268. {oold-2.2.0 → oold-2.3.0}/tests/test_validation/test_meta_store.py +0 -0
  269. {oold-2.2.0 → oold-2.3.0}/tests/test_validation/test_meta_vendor.py +0 -0
  270. {oold-2.2.0 → oold-2.3.0}/tests/test_validation/test_parity_live.py +0 -0
  271. {oold-2.2.0 → oold-2.3.0}/tests/test_validation/test_pipeline.py +0 -0
  272. {oold-2.2.0 → oold-2.3.0}/tests/test_validation/test_resolve.py +0 -0
  273. {oold-2.2.0 → oold-2.3.0}/tests/test_validation/test_rules.py +0 -0
  274. {oold-2.2.0 → oold-2.3.0}/tests/test_validation/test_schema_levels.py +0 -0
  275. {oold-2.2.0 → oold-2.3.0}/tests/typing/links.py +0 -0
  276. {oold-2.2.0 → oold-2.3.0}/tests/typing/pyrightconfig.json +0 -0
  277. {oold-2.2.0 → oold-2.3.0}/tests/typing/query_dsl.py +0 -0
  278. {oold-2.2.0 → oold-2.3.0}/tests/ui/panel/conftest.py +0 -0
  279. {oold-2.2.0 → oold-2.3.0}/tests/ui/panel/panel_app.py +0 -0
  280. {oold-2.2.0 → oold-2.3.0}/tests/ui/panel/panel_frontend_test.py +0 -0
  281. {oold-2.2.0 → oold-2.3.0}/tox.ini +0 -0
  282. {oold-2.2.0 → oold-2.3.0}/zensical.toml +0 -0
@@ -7,6 +7,31 @@ not edit released sections by hand.
7
7
 
8
8
  <!-- version list -->
9
9
 
10
+ ## v2.3.0 (2026-10-15)
11
+
12
+ ### Bug Fixes
13
+
14
+ - One precedence rule for controllers, isolated per pydantic layer
15
+ ([`2b3f70d`](https://github.com/OO-LD/oold-python/commit/2b3f70dc27d42a508a90ebaa4eeca1dae0fd16a2))
16
+
17
+ - Quieter registry logging, and controllers for inline documents
18
+ ([`37c578e`](https://github.com/OO-LD/oold-python/commit/37c578e7b81f91075e5c6ef2ce9ed993ad0e4dda))
19
+
20
+ ### Features
21
+
22
+ - Let a class opt out of the type registries
23
+ ([`e501f01`](https://github.com/OO-LD/oold-python/commit/e501f0103e488d2354cd3b34a0561e1fd146c125))
24
+
25
+ - Public lookup for the controller type registry
26
+ ([`e777b70`](https://github.com/OO-LD/oold-python/commit/e777b7091484f6aad4edbe3cc3e0419deccc23df))
27
+
28
+ - Public registry lookup for the pydantic v1 models
29
+ ([`84b76cb`](https://github.com/OO-LD/oold-python/commit/84b76cb003e9560dc1c6c2d2262c0e363dfcc85b))
30
+
31
+ - **auth**: Expose the registered credentials
32
+ ([`12e979b`](https://github.com/OO-LD/oold-python/commit/12e979b65e2acfe1a5342c0118df6ef6206108b2))
33
+
34
+
10
35
  ## v2.2.0 (2026-09-27)
11
36
 
12
37
  ### Features
@@ -3,7 +3,7 @@ message: "If you use this software, please cite it as below."
3
3
  type: software
4
4
  title: "oold-python: Object-Oriented Linked Data for Python"
5
5
  # Managed by python-semantic-release (version_variables). Do not edit by hand.
6
- version: 2.2.0
6
+ version: 2.3.0
7
7
  abstract: "A Python package for abstract and object-oriented access to knowledge graphs, bridging semantic web technologies (RDF, JSON-LD, SPARQL) with Pydantic type safety."
8
8
  repository-code: "https://github.com/oo-ld/oold-python"
9
9
  url: "https://OO-LD.github.io/oold-python/"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: oold
3
- Version: 2.2.0
3
+ Version: 2.3.0
4
4
  Summary: Linked data class python package
5
5
  Project-URL: Homepage, https://OO-LD.github.io/oold-python/
6
6
  Project-URL: Documentation, https://OO-LD.github.io/oold-python/
@@ -74,7 +74,33 @@ print(ctrl.to_json())
74
74
 
75
75
  ## Type registry behavior
76
76
 
77
- `BaseController` excludes itself from the `_types` lookup. When the backend resolves `"ex:robot-1"`, it returns a plain `Robot` instance, not a `RobotController`. This prevents controller logic from leaking into resolved data objects.
77
+ A controller is kept out of `_types`, the data model registry, and collected in `_controller_types` instead. The data model therefore stays the class a type IRI maps to, and `to_json()` keeps producing controller-free documents.
78
+
79
+ Resolution still hands out the controller: `from_json()`, `from_jsonld()` and anything else going through `resolve_type()` return `RobotController` for `"ex:robot-1"`, so behaviour is available wherever a model is read back. Pass `prefer_controller=False` to `resolve_type()` to get the pure `Robot` class.
80
+
81
+ When several controllers answer to the same IRI, the one registered **last** wins - a package imported later overrides an implementation without the earlier one knowing about it. Every registration is logged at `INFO` under the `oold.model` logger, and an override at `WARNING`, so an unexpected winner is visible:
82
+
83
+ ```python
84
+ import logging
85
+ logging.getLogger("oold.model").setLevel(logging.INFO)
86
+ ```
87
+
88
+ Resolution never crosses pydantic versions: `oold.model` and `oold.model.v1` keep separate registries, and a lookup is answered from the one the model registry belongs to.
89
+
90
+ To pick a controller explicitly, construct it from the data model:
91
+
92
+ ```python
93
+ ctrl = RobotController(robot, connection_url="tcp://192.168.1.10:5000")
94
+ ```
95
+
96
+ A controller that reuses a model without being the handler for it - a simulator over `Robot`, an in-memory buffer over a local file - sets `__oold_register__ = False` and stays out of the lookup. It is still constructed from the model as above:
97
+
98
+ ```python
99
+ class SimulatedRobot(BaseController, Robot):
100
+ __oold_register__ = False
101
+ ```
102
+
103
+ The flag is inherited, so a subclass of `SimulatedRobot` stays out of the registries too; set `__oold_register__ = True` on it to opt back in. `register_controller_type()` registers an opted-out class regardless - the flag governs the automatic registration on class creation, not an explicit request.
78
104
 
79
105
  ---
80
106
 
@@ -9,7 +9,7 @@ keywords = ['python', 'linked-data', 'json-ld', 'pydantic', 'oo-ld']
9
9
  requires-python = ">=3.10,<4.0"
10
10
  # Version is managed by python-semantic-release (see [tool.semantic_release]).
11
11
  # Do not edit by hand; it is bumped automatically on merge to main.
12
- version = "2.2.0"
12
+ version = "2.3.0"
13
13
  classifiers = [
14
14
  "Development Status :: 4 - Beta",
15
15
  "Intended Audience :: Developers",
@@ -105,6 +105,16 @@ def set_credential(credential: BaseCredential):
105
105
  _credentials.set(creds)
106
106
 
107
107
 
108
+ def registered_credentials() -> dict[str, BaseCredential]:
109
+ """The credentials registered in this context, keyed by IRI.
110
+
111
+ A caller that has to build a connection without being told which one - a
112
+ file controller that was handed no session - needs to see what is on offer
113
+ before it can pick, or report that the choice is ambiguous.
114
+ """
115
+ return dict(_credentials.get())
116
+
117
+
108
118
  def get_credential(iri: str, exact: bool = False) -> BaseCredential:
109
119
  """Retrieve the credential for the given IRI from the global store.
110
120
 
@@ -107,10 +107,108 @@ class FieldProxy:
107
107
  _types: dict[str, pydantic.main._model_construction.ModelMetaclass] = {}
108
108
  _controller_types: dict[str, list] = {}
109
109
 
110
+ _logger = logging.getLogger(__name__)
110
111
 
111
112
  M = TypeVar("M", bound="_LinkedBaseModelLegacy")
112
113
 
113
114
 
115
+ def _cls_name(cls: type) -> str:
116
+ return f"{cls.__module__}.{cls.__qualname__}"
117
+
118
+
119
+ def _usable_iris(cls: type, iri) -> list[str]:
120
+ """The IRIs to register ``cls`` under, flattened and validated.
121
+
122
+ ``get_cls_iri()`` returns a bare string or a list, and the list may hold
123
+ the default of the type field, which is a list again. Flattening here is
124
+ what keeps the public API in step with the metaclass; silently skipping a
125
+ nested or empty shape left a class unregistered and unreachable by type.
126
+ """
127
+ if iri is None:
128
+ iri = cls.get_cls_iri() if hasattr(cls, "get_cls_iri") else None
129
+ if iri is None:
130
+ raise ValueError(f"{_cls_name(cls)} has no type IRI: pass iri= or define get_cls_iri()")
131
+ # filter before sorting: get_cls_iri() may return the default of a required
132
+ # type field, which is PydanticUndefined, and sorting that against a string
133
+ # raises - throwing away the usable IRI next to it
134
+ usable = {value for value in _iri_set(iri) if isinstance(value, str) and value.strip()}
135
+ if not usable:
136
+ raise ValueError(f"{_cls_name(cls)}: no usable type IRI in {iri!r}")
137
+ return sorted(usable)
138
+
139
+
140
+ def registers_itself(cls: type) -> bool:
141
+ """Whether a class wants to be in the registries.
142
+
143
+ A class opts out with ``__oold_register__ = False``. The case for it is a
144
+ controller that reuses another model without being the handler for it - an
145
+ in-memory buffer over a local-file model, say: it is asked for by name and
146
+ constructed from the model, so having it compete for the model's type IRI
147
+ only makes resolution ambiguous.
148
+
149
+ The flag is a plain class attribute, so it is **inherited**: a subclass of
150
+ an opted-out class stays out too, unless it sets ``__oold_register__ =
151
+ True`` itself.
152
+ """
153
+ return bool(getattr(cls, "__oold_register__", True))
154
+
155
+
156
+ def _put_type(iri: str, cls: type, registry: dict | None = None) -> None:
157
+ """Write one model registration, reporting what it replaces.
158
+
159
+ ``registry`` selects the pydantic layer; it defaults to the v2 one. The two
160
+ layers keep separate registries, and a class of the wrong flavour handed to
161
+ deserialization does not validate.
162
+ """
163
+ registry = _types if registry is None else registry
164
+ previous = registry.get(iri)
165
+ if previous is cls:
166
+ return
167
+ if previous is None:
168
+ _logger.info("Type %s: %s", iri, _cls_name(cls))
169
+ elif _cls_name(previous) == _cls_name(cls):
170
+ # the same class from a reloaded module: not an override, and saying
171
+ # "X replaces X" for every class of a regenerated model module is noise
172
+ _logger.debug("Type %s: %s re-registered", iri, _cls_name(cls))
173
+ else:
174
+ _logger.warning(
175
+ "Type %s: %s replaces %s",
176
+ iri,
177
+ _cls_name(cls),
178
+ _cls_name(previous),
179
+ )
180
+ registry[iri] = cls
181
+
182
+
183
+ def _put_controller(iri: str, cls: type, registry: dict | None = None) -> None:
184
+ """Append one controller registration, reporting what it takes over from.
185
+
186
+ A class registered again - re-imported, or defined by a reloaded module -
187
+ moves to the front rather than piling up: resolution takes the last entry,
188
+ so a stale copy left behind would only ever be dead weight.
189
+ """
190
+ registry = _controller_types if registry is None else registry
191
+ entries = registry.setdefault(iri, [])
192
+ if entries and entries[-1] is cls:
193
+ return
194
+ name = _cls_name(cls)
195
+ superseded = [c for c in entries if c is cls or _cls_name(c) == name]
196
+ for stale in superseded:
197
+ entries.remove(stale)
198
+ if entries:
199
+ _logger.warning(
200
+ "Controller %s: %s takes precedence over %s",
201
+ iri,
202
+ name,
203
+ ", ".join(_cls_name(c) for c in entries),
204
+ )
205
+ elif superseded:
206
+ _logger.debug("Controller %s: %s re-registered", iri, name)
207
+ else:
208
+ _logger.info("Controller %s: %s", iri, name)
209
+ entries.append(cls)
210
+
211
+
114
212
  def register_type(cls: type, iri: str | list[str] | None = None) -> None:
115
213
  """Register a model class under the type IRI(s) it answers to.
116
214
 
@@ -122,6 +220,11 @@ def register_type(cls: type, iri: str | list[str] | None = None) -> None:
122
220
  Without it callers reach for the private ``_types`` mapping and write into
123
221
  it directly, which couples them to an internal and offers no validation.
124
222
 
223
+ A class may only claim the IRIs it introduces itself: a subclass that
224
+ merely narrows a field reports its parent's IRI, and registering it would
225
+ make every parent document deserialize as the subclass. Pass ``iri``
226
+ explicitly to override that.
227
+
125
228
  Parameters
126
229
  ----------
127
230
  cls
@@ -129,13 +232,18 @@ def register_type(cls: type, iri: str | list[str] | None = None) -> None:
129
232
  iri
130
233
  The IRI(s) to register under. Defaults to ``cls.get_cls_iri()``.
131
234
  """
132
- if iri is None:
133
- iri = cls.get_cls_iri() if hasattr(cls, "get_cls_iri") else None
134
- if iri is None:
135
- raise ValueError(f"{cls.__name__} has no type IRI: pass iri= or define get_cls_iri()")
136
- for value in iri if isinstance(iri, list) else [iri]:
137
- if isinstance(value, str):
138
- _types[value] = cls
235
+ explicit = iri is not None
236
+ iris = _usable_iris(cls, iri)
237
+ inherited = frozenset() if explicit else _inherited_cls_iris(cls)
238
+ for value in iris:
239
+ if value in inherited:
240
+ _logger.info(
241
+ "%s: %s is inherited, not registering it. Pass iri= to claim it anyway.",
242
+ _cls_name(cls),
243
+ value,
244
+ )
245
+ continue
246
+ _put_type(value, cls)
139
247
 
140
248
 
141
249
  def get_registered_type(iri: str) -> type | None:
@@ -152,7 +260,69 @@ def registered_types() -> dict:
152
260
  return _types
153
261
 
154
262
 
155
- _logger = logging.getLogger(__name__)
263
+ def register_controller_type(cls: type, iri: str | list[str] | None = None) -> None:
264
+ """Register a controller class under the type IRI(s) it can act on.
265
+
266
+ Controller subclasses register themselves on creation, so this is only
267
+ needed for controllers built dynamically or registered under an extra IRI.
268
+ Several controllers may answer to the same IRI; the one registered last
269
+ wins, so a package imported later overrides an implementation.
270
+
271
+ Parameters
272
+ ----------
273
+ cls
274
+ The controller class to register.
275
+ iri
276
+ The IRI(s) to register under. Defaults to ``cls.get_cls_iri()``.
277
+ """
278
+ for value in _usable_iris(cls, iri):
279
+ _put_controller(value, cls)
280
+
281
+
282
+ def get_registered_controller_types(iri: str) -> list:
283
+ """The controller classes registered for a type IRI, last registered first.
284
+
285
+ The order resolution uses: :func:`find_controller_type` and
286
+ :func:`~oold.static.resolve_type` both take the most recent registration.
287
+ """
288
+ return list(reversed(_controller_types.get(iri, [])))
289
+
290
+
291
+ def registered_controller_types() -> dict:
292
+ """The live controller registry.
293
+
294
+ Prefer :func:`register_controller_type` over writing to it directly.
295
+ """
296
+ return _controller_types
297
+
298
+
299
+ def find_controller_type(type_iris, base: type | None = None) -> type | None:
300
+ """A registered controller for any of ``type_iris``.
301
+
302
+ Lets a caller act on an entity without knowing which concrete controller
303
+ implements it: pass the entity's type IRIs and, optionally, the interface
304
+ the controller has to provide. The IRIs are tried in order; within one IRI
305
+ the most recently registered controller wins, so a downstream package can
306
+ override an implementation by importing its own.
307
+
308
+ Parameters
309
+ ----------
310
+ type_iris
311
+ A type IRI or a list of them, tried in order.
312
+ base
313
+ If given, only controllers that are a subclass of it are considered.
314
+
315
+ Returns
316
+ -------
317
+ The controller class, or ``None`` if nothing matches.
318
+ """
319
+ if isinstance(type_iris, str):
320
+ type_iris = [type_iris]
321
+ for iri in type_iris or []:
322
+ for cls in reversed(_controller_types.get(iri, [])):
323
+ if base is None or issubclass(cls, base):
324
+ return cls
325
+ return None
156
326
 
157
327
 
158
328
  def _iri_set(value) -> frozenset:
@@ -214,7 +384,7 @@ class _LinkedBaseModelMetaClassLegacy(pydantic.main._model_construction.ModelMet
214
384
  # (they extend data models but should not replace them in
215
385
  # the type lookup table).
216
386
  _is_ctrl = any(b.__module__ == "oold.model" and b.__name__ == "BaseController" for b in cls.__mro__)
217
- if hasattr(cls, "get_cls_iri"):
387
+ if hasattr(cls, "get_cls_iri") and registers_itself(cls):
218
388
  iri = cls.get_cls_iri()
219
389
  if iri is not None:
220
390
  iris = iri if isinstance(iri, list) else [iri]
@@ -224,10 +394,12 @@ class _LinkedBaseModelMetaClassLegacy(pydantic.main._model_construction.ModelMet
224
394
  # IRIs it introduces itself.
225
395
  inherited = frozenset() if _is_ctrl else _inherited_cls_iris(cls)
226
396
  for i in iris:
397
+ if not isinstance(i, str):
398
+ continue
227
399
  if _is_ctrl:
228
- _controller_types.setdefault(i, []).append(cls)
400
+ _put_controller(i, cls)
229
401
  elif i not in inherited:
230
- _types[i] = cls
402
+ _put_type(i, cls)
231
403
  return cls
232
404
 
233
405
  # override operators, see https://docs.python.org/3/library/operator.html
@@ -1039,7 +1211,7 @@ class _LinkedBaseModelLegacy(BaseModel, GenericLinkedBaseModel, metaclass=_Linke
1039
1211
  @classmethod
1040
1212
  def from_jsonld(cls, jsonld: dict) -> "_LinkedBaseModelLegacy":
1041
1213
  """Constructs a model instance from a JSON-LD representation."""
1042
- return import_jsonld(BaseModel, _LinkedBaseModelLegacy, cls, jsonld, _types)
1214
+ return import_jsonld(BaseModel, _LinkedBaseModelLegacy, cls, jsonld, _types, _controller_types)
1043
1215
 
1044
1216
  def to_json(self, exclude_defaults: bool = False) -> dict:
1045
1217
  """Return the JSON representation of the object.
@@ -1071,7 +1243,7 @@ class _LinkedBaseModelLegacy(BaseModel, GenericLinkedBaseModel, metaclass=_Linke
1071
1243
  @classmethod
1072
1244
  def from_json(cls, data: dict) -> "_LinkedBaseModelLegacy":
1073
1245
  """Constructs a model instance from a JSON representation."""
1074
- return import_json(BaseModel, _LinkedBaseModelLegacy, cls, data, _types)
1246
+ return import_json(BaseModel, _LinkedBaseModelLegacy, cls, data, _types, _controller_types)
1075
1247
 
1076
1248
  # @classmethod
1077
1249
  # def model_json_schema(
@@ -270,21 +270,23 @@ class LinkedApiMixin(GenericLinkedBaseModel):
270
270
 
271
271
  @classmethod
272
272
  def from_json(cls, data: dict[str, Any]) -> Any:
273
+ from oold.model import _controller_types
273
274
  from oold.model._descriptor import _TYPE_REGISTRY
274
275
 
275
276
  # root must be the binding base: import_json only falls back to the
276
277
  # given class when the two differ, which is how a payload without a
277
278
  # type IRI still constructs.
278
- return import_json(BaseModel, cls._root_cls(), cls, data, _TYPE_REGISTRY)
279
+ return import_json(BaseModel, cls._root_cls(), cls, data, _TYPE_REGISTRY, _controller_types)
279
280
 
280
281
  def to_jsonld(self) -> dict[str, Any]:
281
282
  return export_jsonld(self, BaseModel)
282
283
 
283
284
  @classmethod
284
285
  def from_jsonld(cls, jsonld: dict[str, Any]) -> Any:
286
+ from oold.model import _controller_types
285
287
  from oold.model._descriptor import _TYPE_REGISTRY
286
288
 
287
- return import_jsonld(BaseModel, cls._root_cls(), cls, jsonld, _TYPE_REGISTRY)
289
+ return import_jsonld(BaseModel, cls._root_cls(), cls, jsonld, _TYPE_REGISTRY, _controller_types)
288
290
 
289
291
  def store_jsonld(self) -> None:
290
292
  from oold.backend.interface import GetBackendParam, StoreParam, get_backend
@@ -40,6 +40,7 @@ behaviour off entirely and leaves plain pydantic.
40
40
  from __future__ import annotations
41
41
 
42
42
  import contextlib
43
+ import logging
43
44
  import os
44
45
  import sys
45
46
  import types
@@ -649,6 +650,8 @@ def _is_link_annotation(annotation: Any) -> bool:
649
650
  return False
650
651
 
651
652
 
653
+ _logger = logging.getLogger(__name__)
654
+
652
655
  _TYPE_REGISTRY: dict[str, type] = {}
653
656
  """Maps a ``type`` field default (the class IRI) to its model class.
654
657
 
@@ -675,17 +678,61 @@ def use_type_registry(registry: dict) -> None:
675
678
 
676
679
 
677
680
  def _resolve_cls(data: dict[str, Any], target: Any) -> Any:
678
- """Pick the most specific class for a document, by its type IRI."""
681
+ """Pick the most specific class for a document, by its type IRI.
682
+
683
+ A registered controller wins over the pure model, the same way
684
+ ``from_json`` resolves it - otherwise the same document would come back as
685
+ a controller when the link carries an IRI and as a plain model when it is
686
+ inlined.
687
+ """
688
+ from oold.model import _controller_types
689
+
679
690
  type_iri = data.get("type")
680
691
  if isinstance(type_iri, list):
681
692
  type_iri = type_iri[0] if type_iri else None
682
693
  if isinstance(type_iri, str):
694
+ controllers = _controller_types.get(type_iri)
695
+ if controllers:
696
+ return controllers[-1]
683
697
  found = _TYPE_REGISTRY.get(type_iri)
684
698
  if found is not None:
685
699
  return found
686
700
  return target
687
701
 
688
702
 
703
+ def _pure_cls(data: dict[str, Any]) -> Any:
704
+ """The data model for a document's type IRI, ignoring controllers."""
705
+ type_iri = data.get("type")
706
+ if isinstance(type_iri, list):
707
+ type_iri = type_iri[0] if type_iri else None
708
+ return _TYPE_REGISTRY.get(type_iri) if isinstance(type_iri, str) else None
709
+
710
+
711
+ def _construct_resolved(data: dict[str, Any], target: Any) -> Any:
712
+ """Build a linked document, falling back to its pure model.
713
+
714
+ A controller may declare fields the document does not carry. Resolution
715
+ prefers it, so without this a document that used to build as a plain model
716
+ would start raising; ``import_json`` makes the same trade.
717
+ """
718
+ cls = _resolve_cls(data, target)
719
+ if cls is None:
720
+ raise ValueError(f"Cannot construct link from {data!r}: unknown target")
721
+ try:
722
+ return _construct(cls, data)
723
+ except Exception:
724
+ pure = _pure_cls(data)
725
+ if pure is None or pure is cls:
726
+ raise
727
+ _logger.warning(
728
+ "Controller %s could not be built from a linked document "
729
+ "(e.g. missing mandatory fields), falling back to %s",
730
+ cls.__name__,
731
+ pure.__name__,
732
+ )
733
+ return _construct(pure, data)
734
+
735
+
689
736
  class LinkResultList(list[T]):
690
737
  """List returned by a to-many link.
691
738
 
@@ -896,9 +943,7 @@ def _batch_resolve(refs: list[Ref | None], target: Any) -> list[Any]:
896
943
  # a second request whose result was then built against the declared
897
944
  # target, losing the original error and silently mis-constructing.
898
945
  fetched = resolver.resolve_iris(iris)
899
- nodes = {
900
- iri: (_construct(_resolve_cls(d, target), d) if d is not None else None) for iri, d in fetched.items()
901
- }
946
+ nodes = {iri: (_construct_resolved(d, target) if d is not None else None) for iri, d in fetched.items()}
902
947
  for r in group:
903
948
  r._obj = nodes.get(r.iri)
904
949
  return [None if r is None else r._obj for r in refs]
@@ -915,10 +960,7 @@ def _to_ref(value: Any, target: Any) -> Ref | None:
915
960
  return Ref(iri=value, target=target)
916
961
  if isinstance(value, dict):
917
962
  # construct through the model so the linked object is validated
918
- cls = _resolve_cls(value, target)
919
- if cls is None:
920
- raise ValueError(f"Cannot construct link from {value!r}: unknown target")
921
- return Ref(obj=_construct(cls, value), target=target)
963
+ return Ref(obj=_construct_resolved(value, target), target=target)
922
964
  return Ref(obj=value, target=target)
923
965
 
924
966
 
@@ -930,10 +972,16 @@ def _register_class(cls: type) -> None:
930
972
  only claim the IRIs it introduces itself - a subclass that merely narrows a
931
973
  field reports its parent's IRI and would otherwise replace it.
932
974
  """
933
- from oold.model import _controller_types, _inherited_cls_iris
975
+ from oold.model import (
976
+ _controller_types,
977
+ _inherited_cls_iris,
978
+ _put_controller,
979
+ _put_type,
980
+ registers_itself,
981
+ )
934
982
 
935
983
  iri = cls.get_cls_iri() if hasattr(cls, "get_cls_iri") else None
936
- if iri is None:
984
+ if iri is None or not registers_itself(cls):
937
985
  return
938
986
  is_ctrl = any(b.__module__ == "oold.model" and b.__name__ == "BaseController" for b in cls.__mro__)
939
987
  inherited = frozenset() if is_ctrl else _inherited_cls_iris(cls)
@@ -941,9 +989,9 @@ def _register_class(cls: type) -> None:
941
989
  if not isinstance(value, str):
942
990
  continue
943
991
  if is_ctrl:
944
- _controller_types.setdefault(value, []).append(cls)
992
+ _put_controller(value, cls, _controller_types)
945
993
  elif value not in inherited:
946
- _TYPE_REGISTRY[value] = cls
994
+ _put_type(value, cls, _TYPE_REGISTRY)
947
995
 
948
996
 
949
997
  class _AutoLink: