pymetadata 0.6.4__tar.gz → 0.6.6__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 (220) hide show
  1. {pymetadata-0.6.4 → pymetadata-0.6.6}/.bumpversion.toml +1 -1
  2. {pymetadata-0.6.4 → pymetadata-0.6.6}/CITATION.cff +2 -2
  3. {pymetadata-0.6.4 → pymetadata-0.6.6}/PKG-INFO +5 -5
  4. {pymetadata-0.6.4 → pymetadata-0.6.6}/README.md +4 -4
  5. {pymetadata-0.6.4 → pymetadata-0.6.6}/docs/annotations.md +7 -0
  6. {pymetadata-0.6.4 → pymetadata-0.6.6}/docs/development.md +2 -0
  7. {pymetadata-0.6.4 → pymetadata-0.6.6}/docs/index.md +4 -4
  8. {pymetadata-0.6.4 → pymetadata-0.6.6}/pyproject.toml +7 -0
  9. pymetadata-0.6.6/release-notes/0.6.5.md +8 -0
  10. pymetadata-0.6.6/release-notes/0.6.6.md +8 -0
  11. {pymetadata-0.6.4 → pymetadata-0.6.6}/src/pymetadata/__init__.py +1 -1
  12. {pymetadata-0.6.4 → pymetadata-0.6.6}/src/pymetadata/cache.py +17 -6
  13. {pymetadata-0.6.4 → pymetadata-0.6.6}/src/pymetadata/core/annotation.py +28 -9
  14. {pymetadata-0.6.4 → pymetadata-0.6.6}/src/pymetadata/webservices/ols.py +17 -16
  15. {pymetadata-0.6.4 → pymetadata-0.6.6}/tests/core/test_annotation.py +1 -1
  16. pymetadata-0.6.6/tests/test_annotation_resolution.py +108 -0
  17. {pymetadata-0.6.4 → pymetadata-0.6.6}/tests/test_cache.py +36 -0
  18. {pymetadata-0.6.4 → pymetadata-0.6.6}/.github/CODEOWNERS +0 -0
  19. {pymetadata-0.6.4 → pymetadata-0.6.6}/.github/dependabot.yml +0 -0
  20. {pymetadata-0.6.4 → pymetadata-0.6.6}/.github/pull_request_template.md +0 -0
  21. {pymetadata-0.6.4 → pymetadata-0.6.6}/.github/rulesets/apply.sh +0 -0
  22. {pymetadata-0.6.4 → pymetadata-0.6.6}/.github/rulesets/develop.json +0 -0
  23. {pymetadata-0.6.4 → pymetadata-0.6.6}/.github/rulesets/main.json +0 -0
  24. {pymetadata-0.6.4 → pymetadata-0.6.6}/.github/rulesets/tags.json +0 -0
  25. {pymetadata-0.6.4 → pymetadata-0.6.6}/.github/workflows/ci-cd.yml +0 -0
  26. {pymetadata-0.6.4 → pymetadata-0.6.6}/.github/workflows/docs.yml +0 -0
  27. {pymetadata-0.6.4 → pymetadata-0.6.6}/.github/workflows/ruff.yml +0 -0
  28. {pymetadata-0.6.4 → pymetadata-0.6.6}/.github/workflows/ty.yml +0 -0
  29. {pymetadata-0.6.4 → pymetadata-0.6.6}/.gitignore +0 -0
  30. {pymetadata-0.6.4 → pymetadata-0.6.6}/.pre-commit-config.yaml +0 -0
  31. {pymetadata-0.6.4 → pymetadata-0.6.6}/.python-version +0 -0
  32. {pymetadata-0.6.4 → pymetadata-0.6.6}/.ruff.toml +0 -0
  33. {pymetadata-0.6.4 → pymetadata-0.6.6}/.zenodo.json +0 -0
  34. {pymetadata-0.6.4 → pymetadata-0.6.6}/CLAUDE.md +0 -0
  35. {pymetadata-0.6.4 → pymetadata-0.6.6}/LICENSE +0 -0
  36. {pymetadata-0.6.4 → pymetadata-0.6.6}/docs/README.md +0 -0
  37. {pymetadata-0.6.4 → pymetadata-0.6.6}/docs/api/cache.md +0 -0
  38. {pymetadata-0.6.4 → pymetadata-0.6.6}/docs/api/console.md +0 -0
  39. {pymetadata-0.6.4 → pymetadata-0.6.6}/docs/api/core.annotation.md +0 -0
  40. {pymetadata-0.6.4 → pymetadata-0.6.6}/docs/api/core.creator.md +0 -0
  41. {pymetadata-0.6.4 → pymetadata-0.6.6}/docs/api/core.miriam.md +0 -0
  42. {pymetadata-0.6.4 → pymetadata-0.6.6}/docs/api/core.xref.md +0 -0
  43. {pymetadata-0.6.4 → pymetadata-0.6.6}/docs/api/index.md +0 -0
  44. {pymetadata-0.6.4 → pymetadata-0.6.6}/docs/api/log.md +0 -0
  45. {pymetadata-0.6.4 → pymetadata-0.6.6}/docs/api/omex.md +0 -0
  46. {pymetadata-0.6.4 → pymetadata-0.6.6}/docs/api/ontologies.term.md +0 -0
  47. {pymetadata-0.6.4 → pymetadata-0.6.6}/docs/api/webservices.chebi.md +0 -0
  48. {pymetadata-0.6.4 → pymetadata-0.6.6}/docs/api/webservices.ols.md +0 -0
  49. {pymetadata-0.6.4 → pymetadata-0.6.6}/docs/api/webservices.registry.md +0 -0
  50. {pymetadata-0.6.4 → pymetadata-0.6.6}/docs/api/webservices.unichem.md +0 -0
  51. {pymetadata-0.6.4 → pymetadata-0.6.6}/docs/api/webservices.webservice.md +0 -0
  52. {pymetadata-0.6.4 → pymetadata-0.6.6}/docs/images/HARMONY2026_slides.png +0 -0
  53. {pymetadata-0.6.4 → pymetadata-0.6.6}/docs/images/favicon/about.txt +0 -0
  54. {pymetadata-0.6.4 → pymetadata-0.6.6}/docs/images/favicon/android-chrome-192x192.png +0 -0
  55. {pymetadata-0.6.4 → pymetadata-0.6.6}/docs/images/favicon/android-chrome-512x512.png +0 -0
  56. {pymetadata-0.6.4 → pymetadata-0.6.6}/docs/images/favicon/apple-touch-icon.png +0 -0
  57. {pymetadata-0.6.4 → pymetadata-0.6.6}/docs/images/favicon/favicon-16x16.png +0 -0
  58. {pymetadata-0.6.4 → pymetadata-0.6.6}/docs/images/favicon/favicon-32x32.png +0 -0
  59. {pymetadata-0.6.4 → pymetadata-0.6.6}/docs/images/favicon/favicon.ico +0 -0
  60. {pymetadata-0.6.4 → pymetadata-0.6.6}/docs/images/favicon/favicon_io.zip +0 -0
  61. {pymetadata-0.6.4 → pymetadata-0.6.6}/docs/images/favicon/pymetadata-100x100-300dpi.png +0 -0
  62. {pymetadata-0.6.4 → pymetadata-0.6.6}/docs/images/favicon/site.webmanifest +0 -0
  63. {pymetadata-0.6.4 → pymetadata-0.6.6}/docs/installation.md +0 -0
  64. {pymetadata-0.6.4 → pymetadata-0.6.6}/docs/omex.md +0 -0
  65. {pymetadata-0.6.4 → pymetadata-0.6.6}/docs/robots.txt +0 -0
  66. {pymetadata-0.6.4 → pymetadata-0.6.6}/examples/annotation.py +0 -0
  67. {pymetadata-0.6.4 → pymetadata-0.6.6}/examples/cache_path.py +0 -0
  68. {pymetadata-0.6.4 → pymetadata-0.6.6}/examples/omex/biomodels_omex/MODEL2402290004.4_pymetadata.omex +0 -0
  69. {pymetadata-0.6.4 → pymetadata-0.6.6}/examples/omex/biomodels_omex/create_omex.py +0 -0
  70. {pymetadata-0.6.4 → pymetadata-0.6.6}/examples/omex/biomodels_omex/files/BioModelsMetadata_MODEL2402290004.csv +0 -0
  71. {pymetadata-0.6.4 → pymetadata-0.6.6}/examples/omex/biomodels_omex/files/EndoAcuteBZ1.m +0 -0
  72. {pymetadata-0.6.4 → pymetadata-0.6.6}/examples/omex/biomodels_omex/files/EndoAcuteBZ2.m +0 -0
  73. {pymetadata-0.6.4 → pymetadata-0.6.6}/examples/omex/biomodels_omex/files/EndoAcuteBZ3.m +0 -0
  74. {pymetadata-0.6.4 → pymetadata-0.6.6}/examples/omex/biomodels_omex/files/EndoChronicBZ.m +0 -0
  75. {pymetadata-0.6.4 → pymetadata-0.6.6}/examples/omex/biomodels_omex/files/EndoChronicRZ1.m +0 -0
  76. {pymetadata-0.6.4 → pymetadata-0.6.6}/examples/omex/biomodels_omex/files/EndoChronicRZ2.m +0 -0
  77. {pymetadata-0.6.4 → pymetadata-0.6.6}/examples/omex/biomodels_omex/files/EndoNZ.m +0 -0
  78. {pymetadata-0.6.4 → pymetadata-0.6.6}/examples/omex/biomodels_omex/files/POM253.mat +0 -0
  79. {pymetadata-0.6.4 → pymetadata-0.6.6}/examples/omex/biomodels_omex/files/PlotAPCaTTaAcuteAndChronic.m +0 -0
  80. {pymetadata-0.6.4 → pymetadata-0.6.6}/examples/omex/biomodels_omex/files/Zhou2024_Updated-model.m +0 -0
  81. {pymetadata-0.6.4 → pymetadata-0.6.6}/examples/omex/biomodels_omex/files/getCurrentsStructureCaMKSKJrelpKinetics.m +0 -0
  82. {pymetadata-0.6.4 → pymetadata-0.6.6}/examples/omex/biomodels_omex/files/getStartingState.m +0 -0
  83. {pymetadata-0.6.4 → pymetadata-0.6.6}/examples/omex/biomodels_omex/files/main.m +0 -0
  84. {pymetadata-0.6.4 → pymetadata-0.6.6}/examples/omex/biomodels_omex/files/manifest.xml +0 -0
  85. {pymetadata-0.6.4 → pymetadata-0.6.6}/examples/omex/biomodels_omex/files/metadata.rdf +0 -0
  86. {pymetadata-0.6.4 → pymetadata-0.6.6}/examples/omex/biomodels_omex/files/modelRunnerCaMKSKJrelpKinetics1ms.m +0 -0
  87. {pymetadata-0.6.4 → pymetadata-0.6.6}/examples/omex/biomodels_omex/files/model_ToRORd_LandCaMKSKJrelpKinetics.m +0 -0
  88. {pymetadata-0.6.4 → pymetadata-0.6.6}/examples/omex/omex.py +0 -0
  89. {pymetadata-0.6.4 → pymetadata-0.6.6}/examples/omex/omex_from_url.py +0 -0
  90. {pymetadata-0.6.4 → pymetadata-0.6.6}/examples/omex/test.omex +0 -0
  91. {pymetadata-0.6.4 → pymetadata-0.6.6}/examples/pbpko.py +0 -0
  92. {pymetadata-0.6.4 → pymetadata-0.6.6}/examples/sbo.py +0 -0
  93. {pymetadata-0.6.4 → pymetadata-0.6.6}/presentations/HARMONY2026/.gitignore +0 -0
  94. {pymetadata-0.6.4 → pymetadata-0.6.6}/presentations/HARMONY2026/README.md +0 -0
  95. {pymetadata-0.6.4 → pymetadata-0.6.6}/presentations/HARMONY2026/_extensions/grantmcdermott/clean/_extension.yml +0 -0
  96. {pymetadata-0.6.4 → pymetadata-0.6.6}/presentations/HARMONY2026/_extensions/grantmcdermott/clean/clean.scss +0 -0
  97. {pymetadata-0.6.4 → pymetadata-0.6.6}/presentations/HARMONY2026/_extensions/grantmcdermott/clean/mathjax-config.js +0 -0
  98. {pymetadata-0.6.4 → pymetadata-0.6.6}/presentations/HARMONY2026/custom.scss +0 -0
  99. {pymetadata-0.6.4 → pymetadata-0.6.6}/presentations/HARMONY2026/images/annotation_example.png +0 -0
  100. {pymetadata-0.6.4 → pymetadata-0.6.6}/presentations/HARMONY2026/images/annotation_resolved.png +0 -0
  101. {pymetadata-0.6.4 → pymetadata-0.6.6}/presentations/HARMONY2026/images/annotation_resolved_small.png +0 -0
  102. {pymetadata-0.6.4 → pymetadata-0.6.6}/presentations/HARMONY2026/images/github_screenshot.png +0 -0
  103. {pymetadata-0.6.4 → pymetadata-0.6.6}/presentations/HARMONY2026/images/headshot.png +0 -0
  104. {pymetadata-0.6.4 → pymetadata-0.6.6}/presentations/HARMONY2026/images/omex_logo.png +0 -0
  105. {pymetadata-0.6.4 → pymetadata-0.6.6}/presentations/HARMONY2026/images/omex_overview.png +0 -0
  106. {pymetadata-0.6.4 → pymetadata-0.6.6}/presentations/HARMONY2026/images/pymetadata.png +0 -0
  107. {pymetadata-0.6.4 → pymetadata-0.6.6}/presentations/HARMONY2026/images/pymetadata_large.png +0 -0
  108. {pymetadata-0.6.4 → pymetadata-0.6.6}/presentations/HARMONY2026/images/qr_livermetabolism.png +0 -0
  109. {pymetadata-0.6.4 → pymetadata-0.6.6}/presentations/HARMONY2026/mathjax-config.js +0 -0
  110. {pymetadata-0.6.4 → pymetadata-0.6.6}/presentations/HARMONY2026/pymetadata.bib +0 -0
  111. {pymetadata-0.6.4 → pymetadata-0.6.6}/presentations/HARMONY2026/pymetadata.qmd +0 -0
  112. {pymetadata-0.6.4 → pymetadata-0.6.6}/presentations/HARMONY2026/styles.css +0 -0
  113. {pymetadata-0.6.4 → pymetadata-0.6.6}/release-notes/0.0.10.md +0 -0
  114. {pymetadata-0.6.4 → pymetadata-0.6.6}/release-notes/0.0.11.md +0 -0
  115. {pymetadata-0.6.4 → pymetadata-0.6.6}/release-notes/0.0.12.md +0 -0
  116. {pymetadata-0.6.4 → pymetadata-0.6.6}/release-notes/0.0.13.md +0 -0
  117. {pymetadata-0.6.4 → pymetadata-0.6.6}/release-notes/0.0.14.md +0 -0
  118. {pymetadata-0.6.4 → pymetadata-0.6.6}/release-notes/0.0.15.md +0 -0
  119. {pymetadata-0.6.4 → pymetadata-0.6.6}/release-notes/0.0.16.md +0 -0
  120. {pymetadata-0.6.4 → pymetadata-0.6.6}/release-notes/0.0.17.md +0 -0
  121. {pymetadata-0.6.4 → pymetadata-0.6.6}/release-notes/0.0.18.md +0 -0
  122. {pymetadata-0.6.4 → pymetadata-0.6.6}/release-notes/0.0.19.md +0 -0
  123. {pymetadata-0.6.4 → pymetadata-0.6.6}/release-notes/0.0.2.md +0 -0
  124. {pymetadata-0.6.4 → pymetadata-0.6.6}/release-notes/0.0.20.md +0 -0
  125. {pymetadata-0.6.4 → pymetadata-0.6.6}/release-notes/0.0.21.md +0 -0
  126. {pymetadata-0.6.4 → pymetadata-0.6.6}/release-notes/0.0.22.md +0 -0
  127. {pymetadata-0.6.4 → pymetadata-0.6.6}/release-notes/0.0.3.md +0 -0
  128. {pymetadata-0.6.4 → pymetadata-0.6.6}/release-notes/0.0.4.md +0 -0
  129. {pymetadata-0.6.4 → pymetadata-0.6.6}/release-notes/0.0.5.md +0 -0
  130. {pymetadata-0.6.4 → pymetadata-0.6.6}/release-notes/0.0.6.md +0 -0
  131. {pymetadata-0.6.4 → pymetadata-0.6.6}/release-notes/0.0.7.md +0 -0
  132. {pymetadata-0.6.4 → pymetadata-0.6.6}/release-notes/0.0.8.md +0 -0
  133. {pymetadata-0.6.4 → pymetadata-0.6.6}/release-notes/0.0.9.md +0 -0
  134. {pymetadata-0.6.4 → pymetadata-0.6.6}/release-notes/0.1.0.md +0 -0
  135. {pymetadata-0.6.4 → pymetadata-0.6.6}/release-notes/0.2.1.md +0 -0
  136. {pymetadata-0.6.4 → pymetadata-0.6.6}/release-notes/0.2.10.md +0 -0
  137. {pymetadata-0.6.4 → pymetadata-0.6.6}/release-notes/0.2.2.md +0 -0
  138. {pymetadata-0.6.4 → pymetadata-0.6.6}/release-notes/0.2.3.md +0 -0
  139. {pymetadata-0.6.4 → pymetadata-0.6.6}/release-notes/0.2.4.md +0 -0
  140. {pymetadata-0.6.4 → pymetadata-0.6.6}/release-notes/0.2.5.md +0 -0
  141. {pymetadata-0.6.4 → pymetadata-0.6.6}/release-notes/0.2.6.md +0 -0
  142. {pymetadata-0.6.4 → pymetadata-0.6.6}/release-notes/0.2.7.md +0 -0
  143. {pymetadata-0.6.4 → pymetadata-0.6.6}/release-notes/0.2.8.md +0 -0
  144. {pymetadata-0.6.4 → pymetadata-0.6.6}/release-notes/0.2.9.md +0 -0
  145. {pymetadata-0.6.4 → pymetadata-0.6.6}/release-notes/0.3.0.md +0 -0
  146. {pymetadata-0.6.4 → pymetadata-0.6.6}/release-notes/0.3.1.md +0 -0
  147. {pymetadata-0.6.4 → pymetadata-0.6.6}/release-notes/0.3.10.md +0 -0
  148. {pymetadata-0.6.4 → pymetadata-0.6.6}/release-notes/0.3.11.md +0 -0
  149. {pymetadata-0.6.4 → pymetadata-0.6.6}/release-notes/0.3.2.md +0 -0
  150. {pymetadata-0.6.4 → pymetadata-0.6.6}/release-notes/0.3.3.md +0 -0
  151. {pymetadata-0.6.4 → pymetadata-0.6.6}/release-notes/0.3.4.md +0 -0
  152. {pymetadata-0.6.4 → pymetadata-0.6.6}/release-notes/0.3.5.md +0 -0
  153. {pymetadata-0.6.4 → pymetadata-0.6.6}/release-notes/0.3.6.md +0 -0
  154. {pymetadata-0.6.4 → pymetadata-0.6.6}/release-notes/0.3.7.md +0 -0
  155. {pymetadata-0.6.4 → pymetadata-0.6.6}/release-notes/0.3.8.md +0 -0
  156. {pymetadata-0.6.4 → pymetadata-0.6.6}/release-notes/0.3.9.md +0 -0
  157. {pymetadata-0.6.4 → pymetadata-0.6.6}/release-notes/0.4.0.md +0 -0
  158. {pymetadata-0.6.4 → pymetadata-0.6.6}/release-notes/0.4.1.md +0 -0
  159. {pymetadata-0.6.4 → pymetadata-0.6.6}/release-notes/0.4.2.md +0 -0
  160. {pymetadata-0.6.4 → pymetadata-0.6.6}/release-notes/0.4.3.md +0 -0
  161. {pymetadata-0.6.4 → pymetadata-0.6.6}/release-notes/0.4.4.md +0 -0
  162. {pymetadata-0.6.4 → pymetadata-0.6.6}/release-notes/0.5.0.md +0 -0
  163. {pymetadata-0.6.4 → pymetadata-0.6.6}/release-notes/0.5.1.md +0 -0
  164. {pymetadata-0.6.4 → pymetadata-0.6.6}/release-notes/0.5.10.md +0 -0
  165. {pymetadata-0.6.4 → pymetadata-0.6.6}/release-notes/0.5.11.md +0 -0
  166. {pymetadata-0.6.4 → pymetadata-0.6.6}/release-notes/0.5.12.md +0 -0
  167. {pymetadata-0.6.4 → pymetadata-0.6.6}/release-notes/0.5.2.md +0 -0
  168. {pymetadata-0.6.4 → pymetadata-0.6.6}/release-notes/0.5.3.md +0 -0
  169. {pymetadata-0.6.4 → pymetadata-0.6.6}/release-notes/0.5.4.md +0 -0
  170. {pymetadata-0.6.4 → pymetadata-0.6.6}/release-notes/0.5.5.md +0 -0
  171. {pymetadata-0.6.4 → pymetadata-0.6.6}/release-notes/0.5.6.md +0 -0
  172. {pymetadata-0.6.4 → pymetadata-0.6.6}/release-notes/0.5.7.md +0 -0
  173. {pymetadata-0.6.4 → pymetadata-0.6.6}/release-notes/0.5.8.md +0 -0
  174. {pymetadata-0.6.4 → pymetadata-0.6.6}/release-notes/0.5.9.md +0 -0
  175. {pymetadata-0.6.4 → pymetadata-0.6.6}/release-notes/0.6.0.md +0 -0
  176. {pymetadata-0.6.4 → pymetadata-0.6.6}/release-notes/0.6.1.md +0 -0
  177. {pymetadata-0.6.4 → pymetadata-0.6.6}/release-notes/0.6.2.md +0 -0
  178. {pymetadata-0.6.4 → pymetadata-0.6.6}/release-notes/0.6.3.md +0 -0
  179. {pymetadata-0.6.4 → pymetadata-0.6.6}/release-notes/0.6.4.md +0 -0
  180. {pymetadata-0.6.4 → pymetadata-0.6.6}/scripts/llms_txt.py +0 -0
  181. {pymetadata-0.6.4 → pymetadata-0.6.6}/src/pymetadata/console.py +0 -0
  182. {pymetadata-0.6.4 → pymetadata-0.6.6}/src/pymetadata/core/__init__.py +0 -0
  183. {pymetadata-0.6.4 → pymetadata-0.6.6}/src/pymetadata/core/creator.py +0 -0
  184. {pymetadata-0.6.4 → pymetadata-0.6.6}/src/pymetadata/core/miriam.py +0 -0
  185. {pymetadata-0.6.4 → pymetadata-0.6.6}/src/pymetadata/core/synonym.py +0 -0
  186. {pymetadata-0.6.4 → pymetadata-0.6.6}/src/pymetadata/core/xref.py +0 -0
  187. {pymetadata-0.6.4 → pymetadata-0.6.6}/src/pymetadata/log.py +0 -0
  188. {pymetadata-0.6.4 → pymetadata-0.6.6}/src/pymetadata/omex.py +0 -0
  189. {pymetadata-0.6.4 → pymetadata-0.6.6}/src/pymetadata/ontologies/__init__.py +0 -0
  190. {pymetadata-0.6.4 → pymetadata-0.6.6}/src/pymetadata/ontologies/_ontology_builder.py +0 -0
  191. {pymetadata-0.6.4 → pymetadata-0.6.6}/src/pymetadata/ontologies/kisao.py +0 -0
  192. {pymetadata-0.6.4 → pymetadata-0.6.6}/src/pymetadata/ontologies/pbpko.py +0 -0
  193. {pymetadata-0.6.4 → pymetadata-0.6.6}/src/pymetadata/ontologies/sbo.py +0 -0
  194. {pymetadata-0.6.4 → pymetadata-0.6.6}/src/pymetadata/ontologies/term.py +0 -0
  195. {pymetadata-0.6.4 → pymetadata-0.6.6}/src/pymetadata/py.typed +0 -0
  196. {pymetadata-0.6.4 → pymetadata-0.6.6}/src/pymetadata/resources/ontologies/README.md +0 -0
  197. {pymetadata-0.6.4 → pymetadata-0.6.6}/src/pymetadata/webservices/__init__.py +0 -0
  198. {pymetadata-0.6.4 → pymetadata-0.6.6}/src/pymetadata/webservices/chebi.py +0 -0
  199. {pymetadata-0.6.4 → pymetadata-0.6.6}/src/pymetadata/webservices/registry.py +0 -0
  200. {pymetadata-0.6.4 → pymetadata-0.6.6}/src/pymetadata/webservices/unichem.py +0 -0
  201. {pymetadata-0.6.4 → pymetadata-0.6.6}/src/pymetadata/webservices/webservice.py +0 -0
  202. {pymetadata-0.6.4 → pymetadata-0.6.6}/tests/core/test_creator.py +0 -0
  203. {pymetadata-0.6.4 → pymetadata-0.6.6}/tests/data/omex/BIOMD0000000001.omex +0 -0
  204. {pymetadata-0.6.4 → pymetadata-0.6.6}/tests/data/omex/CombineArchiveShowCase.omex +0 -0
  205. {pymetadata-0.6.4 → pymetadata-0.6.6}/tests/data/omex/CombineArchiveShowCase_manifest.xml +0 -0
  206. {pymetadata-0.6.4 → pymetadata-0.6.6}/tests/data/omex/CompModels.omex +0 -0
  207. {pymetadata-0.6.4 → pymetadata-0.6.6}/tests/data/omex/CompModels_manifest.xml +0 -0
  208. {pymetadata-0.6.4 → pymetadata-0.6.6}/tests/data/omex/iCGB21FR.omex +0 -0
  209. {pymetadata-0.6.4 → pymetadata-0.6.6}/tests/test_chebi.py +0 -0
  210. {pymetadata-0.6.4 → pymetadata-0.6.6}/tests/test_log.py +0 -0
  211. {pymetadata-0.6.4 → pymetadata-0.6.6}/tests/test_offline.py +0 -0
  212. {pymetadata-0.6.4 → pymetadata-0.6.6}/tests/test_ols.py +0 -0
  213. {pymetadata-0.6.4 → pymetadata-0.6.6}/tests/test_omex.py +0 -0
  214. {pymetadata-0.6.4 → pymetadata-0.6.6}/tests/test_ontology.py +0 -0
  215. {pymetadata-0.6.4 → pymetadata-0.6.6}/tests/test_registry.py +0 -0
  216. {pymetadata-0.6.4 → pymetadata-0.6.6}/tests/test_sbo_kisao.py +0 -0
  217. {pymetadata-0.6.4 → pymetadata-0.6.6}/tests/test_unichem.py +0 -0
  218. {pymetadata-0.6.4 → pymetadata-0.6.6}/tests/test_webservice.py +0 -0
  219. {pymetadata-0.6.4 → pymetadata-0.6.6}/tox.ini +0 -0
  220. {pymetadata-0.6.4 → pymetadata-0.6.6}/zensical.toml +0 -0
@@ -1,5 +1,5 @@
1
1
  [tool.bumpversion]
2
- current_version = "0.6.4"
2
+ current_version = "0.6.6"
3
3
  commit = true
4
4
  parse = "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)"
5
5
  serialize = ["{major}.{minor}.{patch}"]
@@ -8,8 +8,8 @@ authors:
8
8
  given-names: "Matthias"
9
9
  orcid: "https://orcid.org/0000-0003-1725-179X"
10
10
  affiliation: "Humboldt-University Berlin, Faculty of Life Science, Institute for Biology, Biology, Berlin; University Lübeck; University Hospital Schleswig-Holstein, Campus Lübeck, First Department of Medicine, Germany"
11
- version: "0.6.4"
12
- date-released: "2026-09-07"
11
+ version: "0.6.6"
12
+ date-released: "2026-09-22"
13
13
  doi: "10.5281/zenodo.5308801"
14
14
  license: MIT
15
15
  repository-code: "https://github.com/matthiaskoenig/pymetadata"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: pymetadata
3
- Version: 0.6.4
3
+ Version: 0.6.6
4
4
  Summary: pymetadata are python utilities for working with metadata.
5
5
  Project-URL: Homepage, https://matthiaskoenig.github.io/pymetadata
6
6
  Project-URL: Documentation, https://matthiaskoenig.github.io/pymetadata
@@ -64,7 +64,7 @@ If you have any questions or issues please [open an issue](https://github.com/ma
64
64
 
65
65
  If you use `pymetadata` please cite the archived software on [Zenodo](https://doi.org/10.5281/zenodo.5308801):
66
66
 
67
- > König, M. (2026). *pymetadata are python utilities for working with metadata* (Version 0.6.0) [Computer software]. Zenodo. https://doi.org/10.5281/zenodo.22639628
67
+ > König, M. (2026). *pymetadata are python utilities for working with metadata* (Version 0.6.5) [Computer software]. Zenodo. https://doi.org/10.5281/zenodo.22903506
68
68
 
69
69
  ```bibtex
70
70
  @software{konig_pymetadata,
@@ -72,10 +72,10 @@ If you use `pymetadata` please cite the archived software on [Zenodo](https://do
72
72
  title = {pymetadata are python utilities for working with metadata},
73
73
  year = {2026},
74
74
  month = sep,
75
- version = {0.6.0},
75
+ version = {0.6.5},
76
76
  publisher = {Zenodo},
77
- doi = {10.5281/zenodo.22639628},
78
- url = {https://doi.org/10.5281/zenodo.22639628},
77
+ doi = {10.5281/zenodo.22903506},
78
+ url = {https://doi.org/10.5281/zenodo.22903506},
79
79
  }
80
80
  ```
81
81
 
@@ -19,7 +19,7 @@ If you have any questions or issues please [open an issue](https://github.com/ma
19
19
 
20
20
  If you use `pymetadata` please cite the archived software on [Zenodo](https://doi.org/10.5281/zenodo.5308801):
21
21
 
22
- > König, M. (2026). *pymetadata are python utilities for working with metadata* (Version 0.6.0) [Computer software]. Zenodo. https://doi.org/10.5281/zenodo.22639628
22
+ > König, M. (2026). *pymetadata are python utilities for working with metadata* (Version 0.6.5) [Computer software]. Zenodo. https://doi.org/10.5281/zenodo.22903506
23
23
 
24
24
  ```bibtex
25
25
  @software{konig_pymetadata,
@@ -27,10 +27,10 @@ If you use `pymetadata` please cite the archived software on [Zenodo](https://do
27
27
  title = {pymetadata are python utilities for working with metadata},
28
28
  year = {2026},
29
29
  month = sep,
30
- version = {0.6.0},
30
+ version = {0.6.5},
31
31
  publisher = {Zenodo},
32
- doi = {10.5281/zenodo.22639628},
33
- url = {https://doi.org/10.5281/zenodo.22639628},
32
+ doi = {10.5281/zenodo.22903506},
33
+ url = {https://doi.org/10.5281/zenodo.22903506},
34
34
  }
35
35
  ```
36
36
 
@@ -59,6 +59,13 @@ print(annotation.resource_normalized)
59
59
  # https://identifiers.org/sabiork/1406
60
60
  ```
61
61
 
62
+ Bioregistry URLs such as `https://bioregistry.io/CMO:0000003` also expose a
63
+ collection (`cmo`) and compact term (`CMO:0000003`). Their normalized resource
64
+ remains the original Bioregistry URL. `RDFAnnotationData` resolves configured
65
+ OLS ontologies even when they have no identifiers.org namespace, including
66
+ CMO, DRON, MMO and SCDO. Bioregistry terms are not validated against the
67
+ identifiers.org registry.
68
+
62
69
  ## Validation
63
70
 
64
71
  Validation answers two questions: is the qualifier a real MIRIAM qualifier, and does the term match the pattern the identifiers.org registry defines for its collection?
@@ -162,6 +162,8 @@ uvx ty check
162
162
 
163
163
  The configuration lives in `[tool.ty]` in `pyproject.toml`. Warnings are treated as errors, so the codebase is kept free of diagnostics. Suppress an unavoidable diagnostic with a rule specific `# ty: ignore[rule-name]` rather than a blanket comment.
164
164
 
165
+ `[tool.pyright]` in `pyproject.toml` is not a second type check. It only points the [pyright](https://github.com/microsoft/pyright) language server, which editors and coding agents use for navigation, at the uv environment in `.venv`, so that the imports of the dependencies resolve.
166
+
165
167
  ## Documentation
166
168
 
167
169
  The documentation is built with [Zensical](https://zensical.org/), the static site generator of the Material for MkDocs authors. The sources are markdown files in `docs/`, the site is configured in `zensical.toml` in the repository root. Nothing rendered is committed: the site is built by the `documentation` workflow on every push and published to [matthiaskoenig.github.io/pymetadata](https://matthiaskoenig.github.io/pymetadata) from the `develop` branch.
@@ -70,7 +70,7 @@ If you have any questions or issues please [open an issue](https://github.com/ma
70
70
 
71
71
  If you use `pymetadata` please cite the archived software on [Zenodo](https://doi.org/10.5281/zenodo.5308801):
72
72
 
73
- > König, M. (2026). *pymetadata are python utilities for working with metadata* (Version 0.6.0) [Computer software]. Zenodo. https://doi.org/10.5281/zenodo.22639628
73
+ > König, M. (2026). *pymetadata are python utilities for working with metadata* (Version 0.6.5) [Computer software]. Zenodo. https://doi.org/10.5281/zenodo.22903506
74
74
 
75
75
  ```bibtex
76
76
  @software{konig_pymetadata,
@@ -78,10 +78,10 @@ If you use `pymetadata` please cite the archived software on [Zenodo](https://do
78
78
  title = {pymetadata are python utilities for working with metadata},
79
79
  year = {2026},
80
80
  month = sep,
81
- version = {0.6.0},
81
+ version = {0.6.5},
82
82
  publisher = {Zenodo},
83
- doi = {10.5281/zenodo.22639628},
84
- url = {https://doi.org/10.5281/zenodo.22639628},
83
+ doi = {10.5281/zenodo.22903506},
84
+ url = {https://doi.org/10.5281/zenodo.22903506},
85
85
  }
86
86
  ```
87
87
 
@@ -93,3 +93,10 @@ include = ["src", "tests"]
93
93
  [tool.ty.terminal]
94
94
  # warnings are failures: keep the codebase free of any diagnostic
95
95
  error-on-warning = true
96
+
97
+ [tool.pyright]
98
+ # pyright is not a check of the project, types are checked with ty; this only
99
+ # points the pyright language server of an editor or agent at the uv
100
+ # environment, so that the dependencies resolve
101
+ venvPath = "."
102
+ venv = ".venv"
@@ -0,0 +1,8 @@
1
+ # Release notes for pymetadata 0.6.5
2
+ ![pymetadata](./docs/images/favicon/pymetadata-100x100-300dpi.png)
3
+
4
+ ## Fixes
5
+ - Bioregistry URLs now expose their collection and compact term, allowing `RDFAnnotationData` to resolve metadata through OLS while preserving the original URL. Configured OLS ontologies no longer require an identifiers.org namespace, and DRON, MMO and SCDO are now included (#87).
6
+ - OLS identifier prefixes are stripped once, case-insensitively, with either a colon or underscore separator. Identifiers such as `SIO_001013`, `OBI_0000070` and `chebi:37924` now produce the correct term IRI (#87).
7
+
8
+ Your pymetadata team
@@ -0,0 +1,8 @@
1
+ # Release notes for pymetadata 0.6.6
2
+ ![pymetadata](./docs/images/favicon/pymetadata-100x100-300dpi.png)
3
+
4
+ ## Fixes
5
+
6
+ - Write web service caches atomically so concurrent Python test environments and application processes do not read partial JSON. Failed writes preserve the previous cache (#91).
7
+
8
+ Your pymetadata team
@@ -7,7 +7,7 @@ from pathlib import Path
7
7
  logging.getLogger(__name__).addHandler(logging.NullHandler())
8
8
 
9
9
  __author__ = "Matthias Koenig"
10
- __version__ = "0.6.4"
10
+ __version__ = "0.6.6"
11
11
 
12
12
 
13
13
  program_name: str = "pymetadata"
@@ -15,6 +15,7 @@ whatever was cached before.
15
15
 
16
16
  import json
17
17
  import logging
18
+ import tempfile
18
19
  import time
19
20
  from json.encoder import JSONEncoder
20
21
  from pathlib import Path
@@ -124,7 +125,9 @@ def write_json_cache(
124
125
  ) -> None:
125
126
  """Write a JSON cache file.
126
127
 
127
- Missing parent directories are created.
128
+ Missing parent directories are created. The file is replaced atomically so
129
+ concurrent readers never see partial JSON and failed writes preserve the
130
+ previous cache.
128
131
 
129
132
  Args:
130
133
  data: data to serialize
@@ -133,9 +136,17 @@ def write_json_cache(
133
136
  `DataclassJSONEncoder` for dataclasses
134
137
  """
135
138
  cache_path.parent.mkdir(parents=True, exist_ok=True)
136
- with open(cache_path, "w") as fp:
137
- logger.info("Write cache: %s", cache_path)
138
- if json_encoder:
139
+ temporary_path: Path | None = None
140
+ try:
141
+ # Use the same filesystem for atomic replacement. Close the file before
142
+ # replacing it so this also works on Windows.
143
+ with tempfile.NamedTemporaryFile(
144
+ mode="w", dir=cache_path.parent, delete=False
145
+ ) as fp:
146
+ temporary_path = Path(fp.name)
147
+ logger.info("Write cache: %s", cache_path)
139
148
  json.dump(data, fp=fp, indent=2, cls=json_encoder)
140
- else:
141
- json.dump(data, fp=fp, indent=2)
149
+ temporary_path.replace(cache_path)
150
+ finally:
151
+ if temporary_path is not None:
152
+ temporary_path.unlink(missing_ok=True)
@@ -58,7 +58,9 @@ IDENTIFIERS_ORG_PATTERN_CLASSIC: Final = re.compile(
58
58
  )
59
59
 
60
60
  BIOREGISTRY_PREFIX: Final = "https://bioregistry.io"
61
- BIOREGISTRY_PATTERN: Final = re.compile(r"^https?://bioregistry.io/(.+)")
61
+ BIOREGISTRY_PATTERN: Final = re.compile(
62
+ r"^https?://bioregistry\.io/([a-zA-Z0-9._-]+):([^?#]+)$"
63
+ )
62
64
 
63
65
  MIRIAM_URN_PATTERN: Final = re.compile(r"^urn:miriam:(.+)")
64
66
 
@@ -155,7 +157,12 @@ class RDFAnnotation:
155
157
  # other urls are directly stored as resources without collection
156
158
  self.collection = None
157
159
  self.term = resource
158
- if BIOREGISTRY_PATTERN.match(resource):
160
+ match_bioregistry = BIOREGISTRY_PATTERN.match(resource)
161
+ if match_bioregistry:
162
+ self.collection = match_bioregistry.group(1).lower()
163
+ self.term = (
164
+ f"{match_bioregistry.group(1)}:{match_bioregistry.group(2)}"
165
+ )
159
166
  self.provider = ProviderType.BIOREGISTRY_IO
160
167
  else:
161
168
  self.provider = ProviderType.NONE
@@ -196,7 +203,11 @@ class RDFAnnotation:
196
203
  self.provider = ProviderType.NONE
197
204
 
198
205
  # shorten compact terms
199
- if self.term and self.collection:
206
+ if (
207
+ self.term
208
+ and self.collection
209
+ and self.provider == ProviderType.IDENTIFIERS_ORG
210
+ ):
200
211
  self.term = self.shorten_compact_term(
201
212
  term=self.term, collection=self.collection
202
213
  )
@@ -256,12 +267,15 @@ class RDFAnnotation:
256
267
  `https://identifiers.org/<collection>/<term>`. Whether the term is
257
268
  valid does not matter here, this is the task of `validate`.
258
269
 
259
- Resources without a collection, i.e., arbitrary urls, are returned
260
- unchanged.
270
+ Bioregistry URLs retain their original resource while exposing their
271
+ collection and compact term for OLS resolution. Arbitrary URLs are also
272
+ returned unchanged.
261
273
  """
262
274
  if not self.term:
263
275
  return None
264
276
 
277
+ if self.provider == ProviderType.BIOREGISTRY_IO:
278
+ return self.resource
265
279
  if self.provider != ProviderType.IDENTIFIERS_ORG or self.collection is None:
266
280
  return self.term
267
281
 
@@ -353,12 +367,17 @@ class RDFAnnotation:
353
367
  """Validate qualifier and term of the annotation.
354
368
 
355
369
  Returns:
356
- True if the qualifier is a MIRIAM qualifier and the term matches the
357
- pattern of its collection.
370
+ True if the qualifier is a MIRIAM qualifier and an identifiers.org
371
+ term matches its collection pattern. Other providers do not use
372
+ identifiers.org term validation.
358
373
  """
359
374
  valid_qualifier: bool = self.check_qualifier(self.qualifier)
360
375
  valid_term: bool = True
361
- if self.collection and self.term:
376
+ if (
377
+ self.collection
378
+ and self.term
379
+ and self.provider == ProviderType.IDENTIFIERS_ORG
380
+ ):
362
381
  valid_term = self.check_miriam_term()
363
382
 
364
383
  return valid_qualifier and valid_term
@@ -408,7 +427,7 @@ class RDFAnnotationData(RDFAnnotation):
408
427
  self.warnings: list = []
409
428
  self.errors: list = []
410
429
 
411
- if self.collection:
430
+ if self.collection and self.provider == ProviderType.IDENTIFIERS_ORG:
412
431
  # register MIRIAM xrefs
413
432
  namespace = get_registry().ns_dict.get(self.collection, None)
414
433
  if not namespace:
@@ -20,6 +20,7 @@ See <https://www.ebi.ac.uk/ols4>.
20
20
 
21
21
  import contextlib
22
22
  import logging
23
+ import re
23
24
  import urllib.parse
24
25
  from dataclasses import dataclass, field
25
26
  from pathlib import Path
@@ -70,6 +71,7 @@ ONTOLOGIES = [
70
71
  OLSOntology(name="cmo"),
71
72
  OLSOntology(name="chmo"),
72
73
  OLSOntology(name="doid"),
74
+ OLSOntology(name="dron"),
73
75
  OLSOntology(name="efo", iri_pattern="http://www.ebi.ac.uk/efo/EFO_{$Id}"),
74
76
  OLSOntology(name="fix"),
75
77
  OLSOntology(name="fma"),
@@ -81,12 +83,14 @@ ONTOLOGIES = [
81
83
  OLSOntology(name="mondo"),
82
84
  OLSOntology(name="ncit"),
83
85
  OLSOntology(name="mp"),
86
+ OLSOntology(name="mmo"),
84
87
  OLSOntology(name="oba"),
85
88
  OLSOntology(name="opmi"),
86
89
  OLSOntology(name="omit"),
87
90
  OLSOntology(
88
91
  name="sio", iri_pattern="http://semanticscience.org/resource/SIO_{$Id}"
89
92
  ),
93
+ OLSOntology(name="scdo"),
90
94
  OLSOntology(name="vto"),
91
95
  ]
92
96
 
@@ -146,8 +150,7 @@ class OLSQuery:
146
150
  """
147
151
  ols_ontology: OLSOntology | None = self.ontologies.get(ontology, None)
148
152
  # remove prefix if existing
149
- if term.startswith(ontology.upper()):
150
- term = term.replace(f"{ontology.upper()}:", "")
153
+ term = re.sub(rf"^{re.escape(ontology)}[:_]", "", term, count=1, flags=re.I)
151
154
 
152
155
  if ols_ontology is None:
153
156
  logger.warning(
@@ -177,23 +180,21 @@ class OLSQuery:
177
180
  if not term:
178
181
  return {"errors": [], "warnings": [f"No term: '{ontology}'"]}
179
182
 
180
- namespace = get_registry().ns_dict.get(ontology)
181
- ols_pattern = None
182
- if namespace and namespace.resources:
183
- for ns_resource in namespace.resources:
184
- if ns_resource.providerCode == "ols":
185
- ols_pattern = ns_resource.urlPattern
186
- break
187
-
188
- if not ols_pattern:
189
- return {
190
- "errors": [],
191
- "warnings": [f"'{ontology}' is not on OLS."],
192
- }
193
-
194
183
  if ontology == "taxonomy":
195
184
  ontology = "ncbitaxon"
196
185
 
186
+ # Configured ontologies need no identifiers.org namespace. Bioregistry
187
+ # includes ontologies which are absent from that registry.
188
+ if ontology not in self.ontologies:
189
+ namespace = get_registry().ns_dict.get(ontology)
190
+ if not namespace or not any(
191
+ resource.providerCode == "ols" for resource in namespace.resources or []
192
+ ):
193
+ return {
194
+ "errors": [],
195
+ "warnings": [f"'{ontology}' is not on OLS."],
196
+ }
197
+
197
198
  iri = self.get_iri(ontology=ontology, term=term)
198
199
 
199
200
  # double urlencode iri for OLS
@@ -149,7 +149,7 @@ rdf_annotation_data = [
149
149
  (
150
150
  BQB.IS,
151
151
  "https://bioregistry.io/chebi:15996",
152
- "RDFAnnotation(BQB.IS|None|https://bioregistry.io/chebi:15996|bioregistry.io)",
152
+ "RDFAnnotation(BQB.IS|chebi|chebi:15996|bioregistry.io)",
153
153
  ),
154
154
  (
155
155
  BQB.IS,
@@ -0,0 +1,108 @@
1
+ """Regression tests for resolver URLs and ontology identifiers."""
2
+
3
+ import urllib.parse
4
+ from pathlib import Path
5
+ from types import SimpleNamespace
6
+
7
+ import pytest
8
+
9
+ from pymetadata.core.annotation import (
10
+ BQB,
11
+ ProviderType,
12
+ RDFAnnotation,
13
+ RDFAnnotationData,
14
+ )
15
+ from pymetadata.webservices.ols import ONTOLOGIES, OLSQuery
16
+
17
+
18
+ @pytest.mark.parametrize("prefix", ["CMO", "cmo", "DRON", "MMO", "SCDO"])
19
+ def test_bioregistry_resolves_without_identifiers_namespace(
20
+ prefix: str, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
21
+ ) -> None:
22
+ """Bioregistry terms resolve through configured OLS ontologies."""
23
+
24
+ def unexpected_registry() -> None:
25
+ raise AssertionError(
26
+ "Configured Bioregistry terms need no identifiers registry"
27
+ )
28
+
29
+ monkeypatch.setattr("pymetadata.core.annotation.get_registry", unexpected_registry)
30
+ monkeypatch.setattr("pymetadata.webservices.ols.get_registry", unexpected_registry)
31
+ query = OLSQuery(ONTOLOGIES, cache_path=tmp_path)
32
+ monkeypatch.setattr("pymetadata.core.annotation.get_ols_query", lambda: query)
33
+ urls: list[str] = []
34
+
35
+ def respond(url: str) -> dict:
36
+ urls.append(urllib.parse.unquote(urllib.parse.unquote(url)))
37
+ return {"label": "body height", "description": ["Height of a body."]}
38
+
39
+ monkeypatch.setattr("pymetadata.webservices.ols.get_json", respond)
40
+ resource = f"https://bioregistry.io/{prefix}:0000003"
41
+ annotation = RDFAnnotation(BQB.IS, resource)
42
+ data = RDFAnnotationData(annotation)
43
+
44
+ assert annotation.collection == prefix.lower()
45
+ assert annotation.provider == ProviderType.BIOREGISTRY_IO
46
+ assert annotation.resource_normalized == resource
47
+ assert data.label == "body height"
48
+ assert data.errors == []
49
+ assert data.warnings == []
50
+ assert annotation.validate()
51
+ # Repeated resolution uses the successful OLS response already on disk.
52
+ assert RDFAnnotationData(annotation).label == "body height"
53
+ assert urls == [
54
+ f"https://www.ebi.ac.uk/ols4/api/ontologies/{prefix.lower()}/terms/"
55
+ f"http://purl.obolibrary.org/obo/{prefix.upper()}_0000003"
56
+ ]
57
+
58
+
59
+ @pytest.mark.parametrize(
60
+ "ontology,term,iri",
61
+ [
62
+ ("sio", "SIO_001013", "http://semanticscience.org/resource/SIO_001013"),
63
+ ("obi", "OBI_0000070", "http://purl.obolibrary.org/obo/OBI_0000070"),
64
+ ("chebi", "chebi:37924", "http://purl.obolibrary.org/obo/CHEBI_37924"),
65
+ ("chebi", "CHEBI:37924", "http://purl.obolibrary.org/obo/CHEBI_37924"),
66
+ ("sio", "001013", "http://semanticscience.org/resource/SIO_001013"),
67
+ ],
68
+ )
69
+ def test_ols_iri_prefix(ontology: str, term: str, iri: str, tmp_path: Path) -> None:
70
+ """A local identifier or CURIE contributes its ontology prefix only once."""
71
+ assert OLSQuery(ONTOLOGIES, cache_path=tmp_path).get_iri(ontology, term) == iri
72
+
73
+
74
+ def test_unknown_bioregistry_ontology_is_not_guessed(
75
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch
76
+ ) -> None:
77
+ """An unconfigured ontology without an OLS provider stays unresolved."""
78
+ registry = SimpleNamespace(ns_dict={})
79
+ monkeypatch.setattr("pymetadata.webservices.ols.get_registry", lambda: registry)
80
+ query = OLSQuery(ONTOLOGIES, cache_path=tmp_path)
81
+ monkeypatch.setattr("pymetadata.core.annotation.get_ols_query", lambda: query)
82
+
83
+ def unexpected_request(url: str) -> dict:
84
+ raise AssertionError(f"Unsupported ontology must not be queried: {url}")
85
+
86
+ monkeypatch.setattr("pymetadata.webservices.ols.get_json", unexpected_request)
87
+ data = RDFAnnotationData(
88
+ RDFAnnotation(BQB.IS, "https://bioregistry.io/unknown:123")
89
+ )
90
+ assert data.label is None
91
+ assert data.errors == []
92
+ assert data.warnings == ["'unknown' is not on OLS."]
93
+
94
+
95
+ @pytest.mark.parametrize(
96
+ "resource",
97
+ [
98
+ "https://bioregistryXio/CMO:0000003",
99
+ "https://bioregistry.io/CMO:",
100
+ "https://bioregistry.io/CMO",
101
+ ],
102
+ )
103
+ def test_non_bioregistry_identifier_url_is_preserved(resource: str) -> None:
104
+ """Lookalike hosts and URLs without an accession are arbitrary resources."""
105
+ annotation = RDFAnnotation(BQB.IS, resource)
106
+ assert annotation.provider == ProviderType.NONE
107
+ assert annotation.collection is None
108
+ assert annotation.resource_normalized == resource
@@ -5,6 +5,7 @@ import logging
5
5
  import os
6
6
  import time
7
7
  from pathlib import Path
8
+ from typing import Any
8
9
 
9
10
  import pytest
10
11
 
@@ -114,3 +115,38 @@ def test_write_json_cache_creates_parents(tmp_path: Path) -> None:
114
115
  write_json_cache(data={"a": 1}, cache_path=cache_path)
115
116
 
116
117
  assert json.loads(cache_path.read_text()) == {"a": 1}
118
+
119
+
120
+ def test_write_json_cache_overlapping_access(tmp_path: Path) -> None:
121
+ """Readers and overlapping writers never see partially written JSON."""
122
+ cache_path = tmp_path / "data.json"
123
+ write_json_cache(data={"old": True}, cache_path=cache_path)
124
+
125
+ class OverlappingEncoder(json.JSONEncoder):
126
+ def default(self, o: Any) -> Any:
127
+ """Read and replace the cache while the outer write is in progress."""
128
+ assert read_json_cache(cache_path) == {"old": True}
129
+ write_json_cache(data={"other": "writer"}, cache_path=cache_path)
130
+ assert read_json_cache(cache_path) == {"other": "writer"}
131
+ return "encoded"
132
+
133
+ write_json_cache(
134
+ data={"new": object()},
135
+ cache_path=cache_path,
136
+ json_encoder=OverlappingEncoder,
137
+ )
138
+
139
+ assert read_json_cache(cache_path) == {"new": "encoded"}
140
+ assert list(tmp_path.iterdir()) == [cache_path]
141
+
142
+
143
+ def test_write_json_cache_failure_preserves_cache(tmp_path: Path) -> None:
144
+ """A serialization failure preserves the old cache and removes temporary files."""
145
+ cache_path = tmp_path / "data.json"
146
+ write_json_cache(data={"old": True}, cache_path=cache_path)
147
+
148
+ with pytest.raises(TypeError, match="not JSON serializable"):
149
+ write_json_cache(data={"new": object()}, cache_path=cache_path)
150
+
151
+ assert read_json_cache(cache_path) == {"old": True}
152
+ assert list(tmp_path.iterdir()) == [cache_path]
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes