pymetadata 0.6.2__tar.gz → 0.6.4__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 (217) hide show
  1. {pymetadata-0.6.2 → pymetadata-0.6.4}/.bumpversion.toml +1 -1
  2. {pymetadata-0.6.2 → pymetadata-0.6.4}/.github/workflows/ci-cd.yml +7 -3
  3. {pymetadata-0.6.2 → pymetadata-0.6.4}/.github/workflows/docs.yml +1 -1
  4. {pymetadata-0.6.2 → pymetadata-0.6.4}/.github/workflows/ty.yml +1 -1
  5. {pymetadata-0.6.2 → pymetadata-0.6.4}/CITATION.cff +1 -1
  6. {pymetadata-0.6.2 → pymetadata-0.6.4}/CLAUDE.md +1 -1
  7. {pymetadata-0.6.2 → pymetadata-0.6.4}/PKG-INFO +3 -2
  8. {pymetadata-0.6.2 → pymetadata-0.6.4}/docs/annotations.md +8 -0
  9. {pymetadata-0.6.2 → pymetadata-0.6.4}/docs/development.md +4 -4
  10. {pymetadata-0.6.2 → pymetadata-0.6.4}/docs/installation.md +1 -1
  11. {pymetadata-0.6.2 → pymetadata-0.6.4}/pyproject.toml +2 -1
  12. pymetadata-0.6.4/release-notes/0.6.3.md +16 -0
  13. pymetadata-0.6.4/release-notes/0.6.4.md +13 -0
  14. {pymetadata-0.6.2 → pymetadata-0.6.4}/src/pymetadata/__init__.py +1 -1
  15. {pymetadata-0.6.2 → pymetadata-0.6.4}/src/pymetadata/core/annotation.py +35 -20
  16. {pymetadata-0.6.2 → pymetadata-0.6.4}/src/pymetadata/omex.py +43 -24
  17. {pymetadata-0.6.2 → pymetadata-0.6.4}/src/pymetadata/webservices/unichem.py +59 -32
  18. {pymetadata-0.6.2 → pymetadata-0.6.4}/tests/core/test_annotation.py +79 -0
  19. {pymetadata-0.6.2 → pymetadata-0.6.4}/tests/test_omex.py +63 -0
  20. {pymetadata-0.6.2 → pymetadata-0.6.4}/tests/test_unichem.py +42 -0
  21. {pymetadata-0.6.2 → pymetadata-0.6.4}/tox.ini +1 -1
  22. {pymetadata-0.6.2 → pymetadata-0.6.4}/.github/CODEOWNERS +0 -0
  23. {pymetadata-0.6.2 → pymetadata-0.6.4}/.github/dependabot.yml +0 -0
  24. {pymetadata-0.6.2 → pymetadata-0.6.4}/.github/pull_request_template.md +0 -0
  25. {pymetadata-0.6.2 → pymetadata-0.6.4}/.github/rulesets/apply.sh +0 -0
  26. {pymetadata-0.6.2 → pymetadata-0.6.4}/.github/rulesets/develop.json +0 -0
  27. {pymetadata-0.6.2 → pymetadata-0.6.4}/.github/rulesets/main.json +0 -0
  28. {pymetadata-0.6.2 → pymetadata-0.6.4}/.github/rulesets/tags.json +0 -0
  29. {pymetadata-0.6.2 → pymetadata-0.6.4}/.github/workflows/ruff.yml +0 -0
  30. {pymetadata-0.6.2 → pymetadata-0.6.4}/.gitignore +0 -0
  31. {pymetadata-0.6.2 → pymetadata-0.6.4}/.pre-commit-config.yaml +0 -0
  32. {pymetadata-0.6.2 → pymetadata-0.6.4}/.python-version +0 -0
  33. {pymetadata-0.6.2 → pymetadata-0.6.4}/.ruff.toml +0 -0
  34. {pymetadata-0.6.2 → pymetadata-0.6.4}/.zenodo.json +0 -0
  35. {pymetadata-0.6.2 → pymetadata-0.6.4}/LICENSE +0 -0
  36. {pymetadata-0.6.2 → pymetadata-0.6.4}/README.md +0 -0
  37. {pymetadata-0.6.2 → pymetadata-0.6.4}/docs/README.md +0 -0
  38. {pymetadata-0.6.2 → pymetadata-0.6.4}/docs/api/cache.md +0 -0
  39. {pymetadata-0.6.2 → pymetadata-0.6.4}/docs/api/console.md +0 -0
  40. {pymetadata-0.6.2 → pymetadata-0.6.4}/docs/api/core.annotation.md +0 -0
  41. {pymetadata-0.6.2 → pymetadata-0.6.4}/docs/api/core.creator.md +0 -0
  42. {pymetadata-0.6.2 → pymetadata-0.6.4}/docs/api/core.miriam.md +0 -0
  43. {pymetadata-0.6.2 → pymetadata-0.6.4}/docs/api/core.xref.md +0 -0
  44. {pymetadata-0.6.2 → pymetadata-0.6.4}/docs/api/index.md +0 -0
  45. {pymetadata-0.6.2 → pymetadata-0.6.4}/docs/api/log.md +0 -0
  46. {pymetadata-0.6.2 → pymetadata-0.6.4}/docs/api/omex.md +0 -0
  47. {pymetadata-0.6.2 → pymetadata-0.6.4}/docs/api/ontologies.term.md +0 -0
  48. {pymetadata-0.6.2 → pymetadata-0.6.4}/docs/api/webservices.chebi.md +0 -0
  49. {pymetadata-0.6.2 → pymetadata-0.6.4}/docs/api/webservices.ols.md +0 -0
  50. {pymetadata-0.6.2 → pymetadata-0.6.4}/docs/api/webservices.registry.md +0 -0
  51. {pymetadata-0.6.2 → pymetadata-0.6.4}/docs/api/webservices.unichem.md +0 -0
  52. {pymetadata-0.6.2 → pymetadata-0.6.4}/docs/api/webservices.webservice.md +0 -0
  53. {pymetadata-0.6.2 → pymetadata-0.6.4}/docs/images/HARMONY2026_slides.png +0 -0
  54. {pymetadata-0.6.2 → pymetadata-0.6.4}/docs/images/favicon/about.txt +0 -0
  55. {pymetadata-0.6.2 → pymetadata-0.6.4}/docs/images/favicon/android-chrome-192x192.png +0 -0
  56. {pymetadata-0.6.2 → pymetadata-0.6.4}/docs/images/favicon/android-chrome-512x512.png +0 -0
  57. {pymetadata-0.6.2 → pymetadata-0.6.4}/docs/images/favicon/apple-touch-icon.png +0 -0
  58. {pymetadata-0.6.2 → pymetadata-0.6.4}/docs/images/favicon/favicon-16x16.png +0 -0
  59. {pymetadata-0.6.2 → pymetadata-0.6.4}/docs/images/favicon/favicon-32x32.png +0 -0
  60. {pymetadata-0.6.2 → pymetadata-0.6.4}/docs/images/favicon/favicon.ico +0 -0
  61. {pymetadata-0.6.2 → pymetadata-0.6.4}/docs/images/favicon/favicon_io.zip +0 -0
  62. {pymetadata-0.6.2 → pymetadata-0.6.4}/docs/images/favicon/pymetadata-100x100-300dpi.png +0 -0
  63. {pymetadata-0.6.2 → pymetadata-0.6.4}/docs/images/favicon/site.webmanifest +0 -0
  64. {pymetadata-0.6.2 → pymetadata-0.6.4}/docs/index.md +0 -0
  65. {pymetadata-0.6.2 → pymetadata-0.6.4}/docs/omex.md +0 -0
  66. {pymetadata-0.6.2 → pymetadata-0.6.4}/docs/robots.txt +0 -0
  67. {pymetadata-0.6.2 → pymetadata-0.6.4}/examples/annotation.py +0 -0
  68. {pymetadata-0.6.2 → pymetadata-0.6.4}/examples/cache_path.py +0 -0
  69. {pymetadata-0.6.2 → pymetadata-0.6.4}/examples/omex/biomodels_omex/MODEL2402290004.4_pymetadata.omex +0 -0
  70. {pymetadata-0.6.2 → pymetadata-0.6.4}/examples/omex/biomodels_omex/create_omex.py +0 -0
  71. {pymetadata-0.6.2 → pymetadata-0.6.4}/examples/omex/biomodels_omex/files/BioModelsMetadata_MODEL2402290004.csv +0 -0
  72. {pymetadata-0.6.2 → pymetadata-0.6.4}/examples/omex/biomodels_omex/files/EndoAcuteBZ1.m +0 -0
  73. {pymetadata-0.6.2 → pymetadata-0.6.4}/examples/omex/biomodels_omex/files/EndoAcuteBZ2.m +0 -0
  74. {pymetadata-0.6.2 → pymetadata-0.6.4}/examples/omex/biomodels_omex/files/EndoAcuteBZ3.m +0 -0
  75. {pymetadata-0.6.2 → pymetadata-0.6.4}/examples/omex/biomodels_omex/files/EndoChronicBZ.m +0 -0
  76. {pymetadata-0.6.2 → pymetadata-0.6.4}/examples/omex/biomodels_omex/files/EndoChronicRZ1.m +0 -0
  77. {pymetadata-0.6.2 → pymetadata-0.6.4}/examples/omex/biomodels_omex/files/EndoChronicRZ2.m +0 -0
  78. {pymetadata-0.6.2 → pymetadata-0.6.4}/examples/omex/biomodels_omex/files/EndoNZ.m +0 -0
  79. {pymetadata-0.6.2 → pymetadata-0.6.4}/examples/omex/biomodels_omex/files/POM253.mat +0 -0
  80. {pymetadata-0.6.2 → pymetadata-0.6.4}/examples/omex/biomodels_omex/files/PlotAPCaTTaAcuteAndChronic.m +0 -0
  81. {pymetadata-0.6.2 → pymetadata-0.6.4}/examples/omex/biomodels_omex/files/Zhou2024_Updated-model.m +0 -0
  82. {pymetadata-0.6.2 → pymetadata-0.6.4}/examples/omex/biomodels_omex/files/getCurrentsStructureCaMKSKJrelpKinetics.m +0 -0
  83. {pymetadata-0.6.2 → pymetadata-0.6.4}/examples/omex/biomodels_omex/files/getStartingState.m +0 -0
  84. {pymetadata-0.6.2 → pymetadata-0.6.4}/examples/omex/biomodels_omex/files/main.m +0 -0
  85. {pymetadata-0.6.2 → pymetadata-0.6.4}/examples/omex/biomodels_omex/files/manifest.xml +0 -0
  86. {pymetadata-0.6.2 → pymetadata-0.6.4}/examples/omex/biomodels_omex/files/metadata.rdf +0 -0
  87. {pymetadata-0.6.2 → pymetadata-0.6.4}/examples/omex/biomodels_omex/files/modelRunnerCaMKSKJrelpKinetics1ms.m +0 -0
  88. {pymetadata-0.6.2 → pymetadata-0.6.4}/examples/omex/biomodels_omex/files/model_ToRORd_LandCaMKSKJrelpKinetics.m +0 -0
  89. {pymetadata-0.6.2 → pymetadata-0.6.4}/examples/omex/omex.py +0 -0
  90. {pymetadata-0.6.2 → pymetadata-0.6.4}/examples/omex/omex_from_url.py +0 -0
  91. {pymetadata-0.6.2 → pymetadata-0.6.4}/examples/omex/test.omex +0 -0
  92. {pymetadata-0.6.2 → pymetadata-0.6.4}/examples/pbpko.py +0 -0
  93. {pymetadata-0.6.2 → pymetadata-0.6.4}/examples/sbo.py +0 -0
  94. {pymetadata-0.6.2 → pymetadata-0.6.4}/presentations/HARMONY2026/.gitignore +0 -0
  95. {pymetadata-0.6.2 → pymetadata-0.6.4}/presentations/HARMONY2026/README.md +0 -0
  96. {pymetadata-0.6.2 → pymetadata-0.6.4}/presentations/HARMONY2026/_extensions/grantmcdermott/clean/_extension.yml +0 -0
  97. {pymetadata-0.6.2 → pymetadata-0.6.4}/presentations/HARMONY2026/_extensions/grantmcdermott/clean/clean.scss +0 -0
  98. {pymetadata-0.6.2 → pymetadata-0.6.4}/presentations/HARMONY2026/_extensions/grantmcdermott/clean/mathjax-config.js +0 -0
  99. {pymetadata-0.6.2 → pymetadata-0.6.4}/presentations/HARMONY2026/custom.scss +0 -0
  100. {pymetadata-0.6.2 → pymetadata-0.6.4}/presentations/HARMONY2026/images/annotation_example.png +0 -0
  101. {pymetadata-0.6.2 → pymetadata-0.6.4}/presentations/HARMONY2026/images/annotation_resolved.png +0 -0
  102. {pymetadata-0.6.2 → pymetadata-0.6.4}/presentations/HARMONY2026/images/annotation_resolved_small.png +0 -0
  103. {pymetadata-0.6.2 → pymetadata-0.6.4}/presentations/HARMONY2026/images/github_screenshot.png +0 -0
  104. {pymetadata-0.6.2 → pymetadata-0.6.4}/presentations/HARMONY2026/images/headshot.png +0 -0
  105. {pymetadata-0.6.2 → pymetadata-0.6.4}/presentations/HARMONY2026/images/omex_logo.png +0 -0
  106. {pymetadata-0.6.2 → pymetadata-0.6.4}/presentations/HARMONY2026/images/omex_overview.png +0 -0
  107. {pymetadata-0.6.2 → pymetadata-0.6.4}/presentations/HARMONY2026/images/pymetadata.png +0 -0
  108. {pymetadata-0.6.2 → pymetadata-0.6.4}/presentations/HARMONY2026/images/pymetadata_large.png +0 -0
  109. {pymetadata-0.6.2 → pymetadata-0.6.4}/presentations/HARMONY2026/images/qr_livermetabolism.png +0 -0
  110. {pymetadata-0.6.2 → pymetadata-0.6.4}/presentations/HARMONY2026/mathjax-config.js +0 -0
  111. {pymetadata-0.6.2 → pymetadata-0.6.4}/presentations/HARMONY2026/pymetadata.bib +0 -0
  112. {pymetadata-0.6.2 → pymetadata-0.6.4}/presentations/HARMONY2026/pymetadata.qmd +0 -0
  113. {pymetadata-0.6.2 → pymetadata-0.6.4}/presentations/HARMONY2026/styles.css +0 -0
  114. {pymetadata-0.6.2 → pymetadata-0.6.4}/release-notes/0.0.10.md +0 -0
  115. {pymetadata-0.6.2 → pymetadata-0.6.4}/release-notes/0.0.11.md +0 -0
  116. {pymetadata-0.6.2 → pymetadata-0.6.4}/release-notes/0.0.12.md +0 -0
  117. {pymetadata-0.6.2 → pymetadata-0.6.4}/release-notes/0.0.13.md +0 -0
  118. {pymetadata-0.6.2 → pymetadata-0.6.4}/release-notes/0.0.14.md +0 -0
  119. {pymetadata-0.6.2 → pymetadata-0.6.4}/release-notes/0.0.15.md +0 -0
  120. {pymetadata-0.6.2 → pymetadata-0.6.4}/release-notes/0.0.16.md +0 -0
  121. {pymetadata-0.6.2 → pymetadata-0.6.4}/release-notes/0.0.17.md +0 -0
  122. {pymetadata-0.6.2 → pymetadata-0.6.4}/release-notes/0.0.18.md +0 -0
  123. {pymetadata-0.6.2 → pymetadata-0.6.4}/release-notes/0.0.19.md +0 -0
  124. {pymetadata-0.6.2 → pymetadata-0.6.4}/release-notes/0.0.2.md +0 -0
  125. {pymetadata-0.6.2 → pymetadata-0.6.4}/release-notes/0.0.20.md +0 -0
  126. {pymetadata-0.6.2 → pymetadata-0.6.4}/release-notes/0.0.21.md +0 -0
  127. {pymetadata-0.6.2 → pymetadata-0.6.4}/release-notes/0.0.22.md +0 -0
  128. {pymetadata-0.6.2 → pymetadata-0.6.4}/release-notes/0.0.3.md +0 -0
  129. {pymetadata-0.6.2 → pymetadata-0.6.4}/release-notes/0.0.4.md +0 -0
  130. {pymetadata-0.6.2 → pymetadata-0.6.4}/release-notes/0.0.5.md +0 -0
  131. {pymetadata-0.6.2 → pymetadata-0.6.4}/release-notes/0.0.6.md +0 -0
  132. {pymetadata-0.6.2 → pymetadata-0.6.4}/release-notes/0.0.7.md +0 -0
  133. {pymetadata-0.6.2 → pymetadata-0.6.4}/release-notes/0.0.8.md +0 -0
  134. {pymetadata-0.6.2 → pymetadata-0.6.4}/release-notes/0.0.9.md +0 -0
  135. {pymetadata-0.6.2 → pymetadata-0.6.4}/release-notes/0.1.0.md +0 -0
  136. {pymetadata-0.6.2 → pymetadata-0.6.4}/release-notes/0.2.1.md +0 -0
  137. {pymetadata-0.6.2 → pymetadata-0.6.4}/release-notes/0.2.10.md +0 -0
  138. {pymetadata-0.6.2 → pymetadata-0.6.4}/release-notes/0.2.2.md +0 -0
  139. {pymetadata-0.6.2 → pymetadata-0.6.4}/release-notes/0.2.3.md +0 -0
  140. {pymetadata-0.6.2 → pymetadata-0.6.4}/release-notes/0.2.4.md +0 -0
  141. {pymetadata-0.6.2 → pymetadata-0.6.4}/release-notes/0.2.5.md +0 -0
  142. {pymetadata-0.6.2 → pymetadata-0.6.4}/release-notes/0.2.6.md +0 -0
  143. {pymetadata-0.6.2 → pymetadata-0.6.4}/release-notes/0.2.7.md +0 -0
  144. {pymetadata-0.6.2 → pymetadata-0.6.4}/release-notes/0.2.8.md +0 -0
  145. {pymetadata-0.6.2 → pymetadata-0.6.4}/release-notes/0.2.9.md +0 -0
  146. {pymetadata-0.6.2 → pymetadata-0.6.4}/release-notes/0.3.0.md +0 -0
  147. {pymetadata-0.6.2 → pymetadata-0.6.4}/release-notes/0.3.1.md +0 -0
  148. {pymetadata-0.6.2 → pymetadata-0.6.4}/release-notes/0.3.10.md +0 -0
  149. {pymetadata-0.6.2 → pymetadata-0.6.4}/release-notes/0.3.11.md +0 -0
  150. {pymetadata-0.6.2 → pymetadata-0.6.4}/release-notes/0.3.2.md +0 -0
  151. {pymetadata-0.6.2 → pymetadata-0.6.4}/release-notes/0.3.3.md +0 -0
  152. {pymetadata-0.6.2 → pymetadata-0.6.4}/release-notes/0.3.4.md +0 -0
  153. {pymetadata-0.6.2 → pymetadata-0.6.4}/release-notes/0.3.5.md +0 -0
  154. {pymetadata-0.6.2 → pymetadata-0.6.4}/release-notes/0.3.6.md +0 -0
  155. {pymetadata-0.6.2 → pymetadata-0.6.4}/release-notes/0.3.7.md +0 -0
  156. {pymetadata-0.6.2 → pymetadata-0.6.4}/release-notes/0.3.8.md +0 -0
  157. {pymetadata-0.6.2 → pymetadata-0.6.4}/release-notes/0.3.9.md +0 -0
  158. {pymetadata-0.6.2 → pymetadata-0.6.4}/release-notes/0.4.0.md +0 -0
  159. {pymetadata-0.6.2 → pymetadata-0.6.4}/release-notes/0.4.1.md +0 -0
  160. {pymetadata-0.6.2 → pymetadata-0.6.4}/release-notes/0.4.2.md +0 -0
  161. {pymetadata-0.6.2 → pymetadata-0.6.4}/release-notes/0.4.3.md +0 -0
  162. {pymetadata-0.6.2 → pymetadata-0.6.4}/release-notes/0.4.4.md +0 -0
  163. {pymetadata-0.6.2 → pymetadata-0.6.4}/release-notes/0.5.0.md +0 -0
  164. {pymetadata-0.6.2 → pymetadata-0.6.4}/release-notes/0.5.1.md +0 -0
  165. {pymetadata-0.6.2 → pymetadata-0.6.4}/release-notes/0.5.10.md +0 -0
  166. {pymetadata-0.6.2 → pymetadata-0.6.4}/release-notes/0.5.11.md +0 -0
  167. {pymetadata-0.6.2 → pymetadata-0.6.4}/release-notes/0.5.12.md +0 -0
  168. {pymetadata-0.6.2 → pymetadata-0.6.4}/release-notes/0.5.2.md +0 -0
  169. {pymetadata-0.6.2 → pymetadata-0.6.4}/release-notes/0.5.3.md +0 -0
  170. {pymetadata-0.6.2 → pymetadata-0.6.4}/release-notes/0.5.4.md +0 -0
  171. {pymetadata-0.6.2 → pymetadata-0.6.4}/release-notes/0.5.5.md +0 -0
  172. {pymetadata-0.6.2 → pymetadata-0.6.4}/release-notes/0.5.6.md +0 -0
  173. {pymetadata-0.6.2 → pymetadata-0.6.4}/release-notes/0.5.7.md +0 -0
  174. {pymetadata-0.6.2 → pymetadata-0.6.4}/release-notes/0.5.8.md +0 -0
  175. {pymetadata-0.6.2 → pymetadata-0.6.4}/release-notes/0.5.9.md +0 -0
  176. {pymetadata-0.6.2 → pymetadata-0.6.4}/release-notes/0.6.0.md +0 -0
  177. {pymetadata-0.6.2 → pymetadata-0.6.4}/release-notes/0.6.1.md +0 -0
  178. {pymetadata-0.6.2 → pymetadata-0.6.4}/release-notes/0.6.2.md +0 -0
  179. {pymetadata-0.6.2 → pymetadata-0.6.4}/scripts/llms_txt.py +0 -0
  180. {pymetadata-0.6.2 → pymetadata-0.6.4}/src/pymetadata/cache.py +0 -0
  181. {pymetadata-0.6.2 → pymetadata-0.6.4}/src/pymetadata/console.py +0 -0
  182. {pymetadata-0.6.2 → pymetadata-0.6.4}/src/pymetadata/core/__init__.py +0 -0
  183. {pymetadata-0.6.2 → pymetadata-0.6.4}/src/pymetadata/core/creator.py +0 -0
  184. {pymetadata-0.6.2 → pymetadata-0.6.4}/src/pymetadata/core/miriam.py +0 -0
  185. {pymetadata-0.6.2 → pymetadata-0.6.4}/src/pymetadata/core/synonym.py +0 -0
  186. {pymetadata-0.6.2 → pymetadata-0.6.4}/src/pymetadata/core/xref.py +0 -0
  187. {pymetadata-0.6.2 → pymetadata-0.6.4}/src/pymetadata/log.py +0 -0
  188. {pymetadata-0.6.2 → pymetadata-0.6.4}/src/pymetadata/ontologies/__init__.py +0 -0
  189. {pymetadata-0.6.2 → pymetadata-0.6.4}/src/pymetadata/ontologies/_ontology_builder.py +0 -0
  190. {pymetadata-0.6.2 → pymetadata-0.6.4}/src/pymetadata/ontologies/kisao.py +0 -0
  191. {pymetadata-0.6.2 → pymetadata-0.6.4}/src/pymetadata/ontologies/pbpko.py +0 -0
  192. {pymetadata-0.6.2 → pymetadata-0.6.4}/src/pymetadata/ontologies/sbo.py +0 -0
  193. {pymetadata-0.6.2 → pymetadata-0.6.4}/src/pymetadata/ontologies/term.py +0 -0
  194. {pymetadata-0.6.2 → pymetadata-0.6.4}/src/pymetadata/py.typed +0 -0
  195. {pymetadata-0.6.2 → pymetadata-0.6.4}/src/pymetadata/resources/ontologies/README.md +0 -0
  196. {pymetadata-0.6.2 → pymetadata-0.6.4}/src/pymetadata/webservices/__init__.py +0 -0
  197. {pymetadata-0.6.2 → pymetadata-0.6.4}/src/pymetadata/webservices/chebi.py +0 -0
  198. {pymetadata-0.6.2 → pymetadata-0.6.4}/src/pymetadata/webservices/ols.py +0 -0
  199. {pymetadata-0.6.2 → pymetadata-0.6.4}/src/pymetadata/webservices/registry.py +0 -0
  200. {pymetadata-0.6.2 → pymetadata-0.6.4}/src/pymetadata/webservices/webservice.py +0 -0
  201. {pymetadata-0.6.2 → pymetadata-0.6.4}/tests/core/test_creator.py +0 -0
  202. {pymetadata-0.6.2 → pymetadata-0.6.4}/tests/data/omex/BIOMD0000000001.omex +0 -0
  203. {pymetadata-0.6.2 → pymetadata-0.6.4}/tests/data/omex/CombineArchiveShowCase.omex +0 -0
  204. {pymetadata-0.6.2 → pymetadata-0.6.4}/tests/data/omex/CombineArchiveShowCase_manifest.xml +0 -0
  205. {pymetadata-0.6.2 → pymetadata-0.6.4}/tests/data/omex/CompModels.omex +0 -0
  206. {pymetadata-0.6.2 → pymetadata-0.6.4}/tests/data/omex/CompModels_manifest.xml +0 -0
  207. {pymetadata-0.6.2 → pymetadata-0.6.4}/tests/data/omex/iCGB21FR.omex +0 -0
  208. {pymetadata-0.6.2 → pymetadata-0.6.4}/tests/test_cache.py +0 -0
  209. {pymetadata-0.6.2 → pymetadata-0.6.4}/tests/test_chebi.py +0 -0
  210. {pymetadata-0.6.2 → pymetadata-0.6.4}/tests/test_log.py +0 -0
  211. {pymetadata-0.6.2 → pymetadata-0.6.4}/tests/test_offline.py +0 -0
  212. {pymetadata-0.6.2 → pymetadata-0.6.4}/tests/test_ols.py +0 -0
  213. {pymetadata-0.6.2 → pymetadata-0.6.4}/tests/test_ontology.py +0 -0
  214. {pymetadata-0.6.2 → pymetadata-0.6.4}/tests/test_registry.py +0 -0
  215. {pymetadata-0.6.2 → pymetadata-0.6.4}/tests/test_sbo_kisao.py +0 -0
  216. {pymetadata-0.6.2 → pymetadata-0.6.4}/tests/test_webservice.py +0 -0
  217. {pymetadata-0.6.2 → pymetadata-0.6.4}/zensical.toml +0 -0
@@ -1,5 +1,5 @@
1
1
  [tool.bumpversion]
2
- current_version = "0.6.2"
2
+ current_version = "0.6.4"
3
3
  commit = true
4
4
  parse = "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)"
5
5
  serialize = ["{major}.{minor}.{patch}"]
@@ -26,7 +26,7 @@ jobs:
26
26
  fail-fast: false
27
27
  matrix:
28
28
  os: [ubuntu-latest, windows-latest, macos-latest]
29
- python-version: ["3.11", "3.12", "3.13", "3.14"]
29
+ python-version: ["3.11", "3.12", "3.13", "3.14", "3.15"]
30
30
  # linux covers the full range, windows and macos only the newest version
31
31
  exclude:
32
32
  - os: windows-latest
@@ -35,12 +35,16 @@ jobs:
35
35
  python-version: "3.12"
36
36
  - os: windows-latest
37
37
  python-version: "3.13"
38
+ - os: windows-latest
39
+ python-version: "3.14"
38
40
  - os: macos-latest
39
41
  python-version: "3.11"
40
42
  - os: macos-latest
41
43
  python-version: "3.12"
42
44
  - os: macos-latest
43
45
  python-version: "3.13"
46
+ - os: macos-latest
47
+ python-version: "3.14"
44
48
 
45
49
  steps:
46
50
  - uses: actions/checkout@v7
@@ -48,7 +52,7 @@ jobs:
48
52
  persist-credentials: false
49
53
  - name: Install uv and set the python version
50
54
  # astral-sh/setup-uv publishes no floating major tag beyond v7
51
- uses: astral-sh/setup-uv@v10.0.1
55
+ uses: astral-sh/setup-uv@v10.1.0
52
56
  with:
53
57
  python-version: ${{ matrix.python-version }}
54
58
  enable-cache: true
@@ -88,7 +92,7 @@ jobs:
88
92
  with:
89
93
  persist-credentials: false
90
94
  - name: Install uv
91
- uses: astral-sh/setup-uv@v10.0.1
95
+ uses: astral-sh/setup-uv@v10.1.0
92
96
  with:
93
97
  python-version: "3.14"
94
98
  enable-cache: true
@@ -29,7 +29,7 @@ jobs:
29
29
  - name: Configure GitHub Pages
30
30
  uses: actions/configure-pages@v6
31
31
  - name: Install uv and set the python version
32
- uses: astral-sh/setup-uv@v10.0.1
32
+ uses: astral-sh/setup-uv@v10.1.0
33
33
  with:
34
34
  python-version: "3.14"
35
35
  enable-cache: true
@@ -26,7 +26,7 @@ jobs:
26
26
  with:
27
27
  persist-credentials: false
28
28
  - name: Install uv and set the python version
29
- uses: astral-sh/setup-uv@v10.0.1
29
+ uses: astral-sh/setup-uv@v10.1.0
30
30
  with:
31
31
  python-version: "3.14"
32
32
  enable-cache: true
@@ -8,7 +8,7 @@ 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.2"
11
+ version: "0.6.4"
12
12
  date-released: "2026-09-07"
13
13
  doi: "10.5281/zenodo.5308801"
14
14
  license: MIT
@@ -22,7 +22,7 @@ uv run pre-commit install
22
22
  pytest # all tests
23
23
  pytest tests/test_omex.py # single file
24
24
  pytest tests/test_omex.py::test_entry_from_dict # single test
25
- tox r -e py3.14 # single tox env (py3.11-3.14 available)
25
+ tox r -e py3.15 # single tox env (py3.11-3.15 available)
26
26
  tox run-parallel # full matrix + ty
27
27
 
28
28
  # lint / format / types
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: pymetadata
3
- Version: 0.6.2
3
+ Version: 0.6.4
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
@@ -20,6 +20,7 @@ Classifier: Programming Language :: Python :: 3.11
20
20
  Classifier: Programming Language :: Python :: 3.12
21
21
  Classifier: Programming Language :: Python :: 3.13
22
22
  Classifier: Programming Language :: Python :: 3.14
23
+ Classifier: Programming Language :: Python :: 3.15
23
24
  Classifier: Programming Language :: Python :: Implementation :: CPython
24
25
  Classifier: Topic :: Scientific/Engineering
25
26
  Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
@@ -37,7 +38,7 @@ Requires-Dist: pytest>=9.1.1; extra == 'dev'
37
38
  Requires-Dist: ruff>=0.16.6; extra == 'dev'
38
39
  Requires-Dist: tox>=4.61.2; extra == 'dev'
39
40
  Requires-Dist: ty>=0.0.78; extra == 'dev'
40
- Requires-Dist: zensical>=0.0.59; extra == 'dev'
41
+ Requires-Dist: zensical>=0.0.60; extra == 'dev'
41
42
  Provides-Extra: ontology
42
43
  Requires-Dist: pronto>=2.7.3; extra == 'ontology'
43
44
  Description-Content-Type: text/markdown
@@ -51,6 +51,14 @@ RDFAnnotation(qualifier=BQB.IS, resource="https://en.wikipedia.org/wiki/Cytosol"
51
51
 
52
52
  `resource_normalized` returns `https://identifiers.org/<prefix>:<accession>`. For namespaces which embed their prefix in the identifier (GO, CHEBI, SBO, BTO) the prefix is already part of the term and is not added a second time.
53
53
 
54
+ The normalization never changes what a resource says: `resource_normalized` is a URL, and parsing it again gives the same `collection` and `term`. A collection which is not in the identifiers.org registry, or a term which does not carry the prefix of its collection, cannot be written as a compact identifier and is returned as `https://identifiers.org/<collection>/<term>`:
55
+
56
+ ```python
57
+ annotation = RDFAnnotation(qualifier=BQB.IS, resource="urn:miriam:sabiork:1406")
58
+ print(annotation.resource_normalized)
59
+ # https://identifiers.org/sabiork/1406
60
+ ```
61
+
54
62
  ## Validation
55
63
 
56
64
  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?
@@ -27,7 +27,7 @@ A pull request can only be merged once the four required checks are green:
27
27
 
28
28
  | check | workflow | content |
29
29
  | ------- | ------------- | -------------------------------------------------------------------- |
30
- | `tests` | `ci-cd.yml` | the test matrix, linux, macos and windows with python 3.11 to 3.14 |
30
+ | `tests` | `ci-cd.yml` | the test matrix, linux, macos and windows with python 3.11 to 3.15 |
31
31
  | `ruff` | `ruff.yml` | `ruff check` and `ruff format --check` |
32
32
  | `ty` | `ty.yml` | `tox r -e ty` |
33
33
  | `docs` | `docs.yml` | the zensical build including the api reference and the agent files |
@@ -118,16 +118,16 @@ From now on every commit is checked with ruff (lint and format) and ty, i.e., th
118
118
 
119
119
  The tests are written with pytest, tox runs them against every supported python version.
120
120
 
121
- The tox environments are named after the interpreter (`py3.11` to `py3.14`, see `envlist` in `tox.ini`), a single one is run with
121
+ The tox environments are named after the interpreter (`py3.11` to `py3.15`, see `envlist` in `tox.ini`), a single one is run with
122
122
  ```bash
123
- tox r -e py3.14
123
+ tox r -e py3.15
124
124
  ```
125
125
  and the complete matrix, including the `ty` environment, in parallel with
126
126
  ```bash
127
127
  tox run-parallel
128
128
  ```
129
129
 
130
- This needs the interpreters to be available, which uv installs with `uv python install 3.11 3.12 3.13 3.14`. Continuous integration runs the same environments as `uvx --with tox-uv tox -e py3.14`.
130
+ This needs the interpreters to be available, which uv installs with `uv python install 3.11 3.12 3.13 3.14 3.15`. Continuous integration runs the same environments as `uvx --with tox-uv tox -e py3.15`.
131
131
 
132
132
  To run the tests directly against the development environment use
133
133
 
@@ -1,6 +1,6 @@
1
1
  # Installation
2
2
 
3
- `pymetadata` requires python >= 3.11 and is available from [pypi](https://pypi.python.org/pypi/pymetadata). It is pure python without compiled dependencies, so the installation is the same on Linux, macOS and Windows.
3
+ `pymetadata` requires python >= 3.11 and is available from [pypi](https://pypi.python.org/pypi/pymetadata). It is pure python and tested with python 3.11 to 3.15 on Linux, macOS and Windows. The only compiled dependency is `pydantic-core`, which comes with [pydantic](https://docs.pydantic.dev/) and is installed from a wheel; for python 3.15 these wheels are part of pydantic 2.14, with an older pydantic it is built from source, which requires a Rust toolchain.
4
4
 
5
5
  ## With uv
6
6
 
@@ -24,6 +24,7 @@ classifiers = [
24
24
  "Programming Language :: Python :: 3.12",
25
25
  "Programming Language :: Python :: 3.13",
26
26
  "Programming Language :: Python :: 3.14",
27
+ "Programming Language :: Python :: 3.15",
27
28
  "Programming Language :: Python :: Implementation :: CPython",
28
29
  "Topic :: Scientific/Engineering",
29
30
  "Topic :: Scientific/Engineering :: Bio-Informatics",
@@ -57,7 +58,7 @@ dev = [
57
58
  "tox>=4.61.2",
58
59
  "pytest>=9.1.1",
59
60
  "pytest-cov>=7.1.0",
60
- "zensical>=0.0.59",
61
+ "zensical>=0.0.60",
61
62
  "mkdocstrings-python>=2.0.8",
62
63
  ]
63
64
 
@@ -0,0 +1,16 @@
1
+ # Release notes for pymetadata 0.6.3
2
+ ![pymetadata](./docs/images/favicon/pymetadata-100x100-300dpi.png)
3
+
4
+ We are pleased to release the next version of pymetadata including the
5
+ following changes:
6
+
7
+ ## Features
8
+ - support for python 3.15, which is part of the test matrix and the tox environments
9
+
10
+ ## Fixes
11
+ - `UnichemQuery` works with the current UniChem web service again, which changed the fields of a source (`lastChecked` and `srcLastUpdated` are new, `lastUpdated`, `srcReleaseDate` and `srcReleaseNumber` were removed). Only `sourceID` and `name` of a `UnichemSource` are required now and `UnichemSource.from_dict` ignores unknown fields, so such a change, or a cache written by an older version, no longer breaks the queries
12
+
13
+ ## Development
14
+ - the development environment requires zensical 0.0.60, the version the documentation is built with
15
+
16
+ Your pymetadata team
@@ -0,0 +1,13 @@
1
+ # Release notes for pymetadata 0.6.4
2
+ ![pymetadata](./docs/images/favicon/pymetadata-100x100-300dpi.png)
3
+
4
+ We are pleased to release the next version of pymetadata including the
5
+ following changes:
6
+
7
+ ## Fixes
8
+ - `Omex.from_directory`, and with it `Omex.from_omex` and `Omex.from_url`, add the entries in the order of the manifest, followed by the files which the manifest does not list, sorted by their location. The entries were added in the order of `os.walk`, which is the order of the file system, so `manifest.entries` and `entries_by_format` of the same archive differed between machines
9
+ - `RDFAnnotation.resource_normalized` keeps the collection and the term of every resource: the result is a URL, and parsing it again gives the same `collection` and `term`. A collection which is not in the identifiers.org registry (`http://identifiers.org/sabiork/1406`, `urn:miriam:foo:bar`) was reduced to the bare term, and a term which does not match the pattern of its collection (`http://identifiers.org/chebi/000000035`) lost its collection. Both are now returned as `https://identifiers.org/<collection>/<term>`, and the compact identifier of a prefix which is not in the registry stays the URL `https://identifiers.org/CMO:0000012` (#81)
10
+ - identifiers.org URLs of a prefix with a hyphen or an underscore, like `ec-code`, `pdb-ccd` or `go_ref`, are parsed into collection and term instead of being kept as an arbitrary URL (#81)
11
+ - the collection of a MIRIAM URN and of the `collection/term` notation is lowercased, as it already was for URLs, so `TAXONOMY/9606` is the collection `taxonomy` (#81)
12
+
13
+ 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.2"
10
+ __version__ = "0.6.4"
11
11
 
12
12
 
13
13
  program_name: str = "pymetadata"
@@ -48,11 +48,13 @@ def get_ols_query() -> OLSQuery:
48
48
 
49
49
 
50
50
  IDENTIFIERS_ORG_PREFIX: Final = "https://identifiers.org"
51
+ # prefixes of the registry consist of letters, digits, `.`, `_` and `-`,
52
+ # e.g., `ec-code` or `go_ref`
51
53
  IDENTIFIERS_ORG_PATTERN_COMPACT: Final = re.compile(
52
- r"^https?://identifiers.org/([a-zA-Z0-9.]+):(.+)"
54
+ r"^https?://identifiers.org/([a-zA-Z0-9._-]+):(.+)"
53
55
  )
54
56
  IDENTIFIERS_ORG_PATTERN_CLASSIC: Final = re.compile(
55
- r"^https?://identifiers.org/([a-zA-Z0-9.]+)/(.+)"
57
+ r"^https?://identifiers.org/([a-zA-Z0-9._-]+)/(.+)"
56
58
  )
57
59
 
58
60
  BIOREGISTRY_PREFIX: Final = "https://bioregistry.io"
@@ -167,21 +169,15 @@ class RDFAnnotation:
167
169
  match3 = MIRIAM_URN_PATTERN.match(resource)
168
170
  if match3:
169
171
  tokens = match3.group(1).split(":")
170
- self.collection = tokens[0]
172
+ self.collection = tokens[0].lower()
171
173
  self.term = ":".join(tokens[1:]).replace("%3A", ":")
172
174
  self.provider = ProviderType.IDENTIFIERS_ORG
173
175
 
174
- logger.warning(
175
- "Deprecated urn pattern `%s` updated: %s",
176
- resource,
177
- self.resource_normalized,
178
- )
179
-
180
176
  else:
181
177
  # handle short notation
182
178
  tokens = resource.split("/")
183
179
  if len(tokens) > 1:
184
- self.collection = tokens[0]
180
+ self.collection = tokens[0].lower()
185
181
  self.term = "/".join(tokens[1:])
186
182
  self.provider = ProviderType.IDENTIFIERS_ORG
187
183
  elif len(tokens) == 1 and ":" in tokens[0]:
@@ -209,6 +205,13 @@ class RDFAnnotation:
209
205
  if self.collection in self.replaced_collections:
210
206
  self.collection = self.replaced_collections[self.collection]
211
207
 
208
+ if resource.startswith("urn:miriam:"):
209
+ logger.warning(
210
+ "Deprecated urn pattern `%s` updated: %s",
211
+ resource,
212
+ self.resource_normalized,
213
+ )
214
+
212
215
  if validate:
213
216
  self.validate()
214
217
 
@@ -244,21 +247,33 @@ class RDFAnnotation:
244
247
  `https://identifiers.org/<prefix>:<accession>`. If the namespace is
245
248
  embedded in the LUI the prefix is already part of the term, otherwise
246
249
  the prefix of the identifiers.org registry is prepended.
250
+
251
+ The normalization never changes what the resource says: the result is
252
+ an url, and parsing it again yields the same `collection` and `term`.
253
+ A collection which is not in the registry, or a term which does not
254
+ carry the prefix of its collection, can not be written as a compact
255
+ identifier and is returned as
256
+ `https://identifiers.org/<collection>/<term>`. Whether the term is
257
+ valid does not matter here, this is the task of `validate`.
258
+
259
+ Resources without a collection, i.e., arbitrary urls, are returned
260
+ unchanged.
247
261
  """
248
262
  if not self.term:
249
263
  return None
250
264
 
251
- if (
252
- self.provider == ProviderType.IDENTIFIERS_ORG
253
- and self.collection is not None
254
- ):
255
- namespace = get_registry().ns_dict.get(self.collection, None)
256
- if namespace:
257
- if namespace.namespaceEmbeddedInLui:
258
- return f"{IDENTIFIERS_ORG_PREFIX}/{self.term}"
259
- return f"{IDENTIFIERS_ORG_PREFIX}/{self.collection}:{self.term}"
265
+ if self.provider != ProviderType.IDENTIFIERS_ORG or self.collection is None:
266
+ return self.term
267
+
268
+ namespace = get_registry().ns_dict.get(self.collection, None)
269
+ if namespace and not namespace.namespaceEmbeddedInLui:
270
+ return f"{IDENTIFIERS_ORG_PREFIX}/{self.collection}:{self.term}"
271
+
272
+ # the term is only a compact identifier if its prefix is the collection
273
+ if self.term.lower().startswith(f"{self.collection}:"):
274
+ return f"{IDENTIFIERS_ORG_PREFIX}/{self.term}"
260
275
 
261
- return self.term
276
+ return f"{IDENTIFIERS_ORG_PREFIX}/{self.collection}/{self.term}"
262
277
 
263
278
  def __repr__(self) -> str:
264
279
  """Get representation string."""
@@ -743,6 +743,10 @@ class Omex:
743
743
  `Omex.guess_format`; SED-ML files added this way get `master=True`,
744
744
  as they are the entry point of a simulation study.
745
745
 
746
+ The entries are in the order of the manifest, followed by the files
747
+ which the manifest does not list, sorted by their location. The order
748
+ does not depend on the file system.
749
+
746
750
  Args:
747
751
  directory: directory with the content of the archive
748
752
 
@@ -785,7 +789,8 @@ class Omex:
785
789
  # new archive
786
790
  omex = Omex()
787
791
 
788
- # iterate over all locations and add entry
792
+ # locations of all files
793
+ file_paths: dict[str, str] = {}
789
794
  for root, _dirs, files in os.walk(str(directory)):
790
795
  for file in files:
791
796
  file_path = os.path.join(root, file)
@@ -795,32 +800,46 @@ class Omex:
795
800
  if location == "./manifest.xml":
796
801
  # manifest is created from the internal manifest entries
797
802
  continue
803
+ file_paths[location] = file_path
804
+
805
+ # The order of `os.walk` is the order of the file system, which differs
806
+ # between machines: the entries are added in the order of the manifest,
807
+ # followed by the files which the manifest does not list, by location.
808
+ positions: dict[str, int] = {}
809
+ if manifest:
810
+ positions = {e.location: k for k, e in enumerate(manifest.entries)}
811
+ locations = sorted(
812
+ file_paths, key=lambda loc: (positions.get(loc, len(positions)), loc)
813
+ )
798
814
 
799
- logger.debug("'%s' -> '%s'", file_path, location)
800
- entry: ManifestEntry
801
- if manifest and location in manifest:
802
- # use entry from existing manifest
803
- entry = manifest[location]
804
- else:
805
- if manifest and location not in manifest:
806
- logger.warning(
807
- "Entry with location missing in manifest.xml: '%s'",
808
- location,
809
- )
810
-
811
- format = Omex.guess_format(Path(file_path))
812
- master = False
813
- if format and ManifestEntry.is_format(
814
- format_key="sedml", format=format
815
- ):
816
- master = True
817
- entry = ManifestEntry(
818
- location=location,
819
- format=format,
820
- master=master,
815
+ # iterate over all locations and add entry
816
+ for location in locations:
817
+ file_path = file_paths[location]
818
+ logger.debug("'%s' -> '%s'", file_path, location)
819
+ entry: ManifestEntry
820
+ if manifest and location in manifest:
821
+ # use entry from existing manifest
822
+ entry = manifest[location]
823
+ else:
824
+ if manifest and location not in manifest:
825
+ logger.warning(
826
+ "Entry with location missing in manifest.xml: '%s'",
827
+ location,
821
828
  )
822
829
 
823
- omex.add_entry(entry_path=Path(file_path), entry=entry)
830
+ format = Omex.guess_format(Path(file_path))
831
+ master = False
832
+ if format and ManifestEntry.is_format(
833
+ format_key="sedml", format=format
834
+ ):
835
+ master = True
836
+ entry = ManifestEntry(
837
+ location=location,
838
+ format=format,
839
+ master=master,
840
+ )
841
+
842
+ omex.add_entry(entry_path=Path(file_path), entry=entry)
824
843
 
825
844
  return omex
826
845
 
@@ -17,9 +17,9 @@ See <https://www.ebi.ac.uk/unichem/info/webservices>.
17
17
  import contextlib
18
18
  import logging
19
19
  import urllib.parse
20
- from dataclasses import dataclass, field
20
+ from dataclasses import dataclass, field, fields
21
21
  from pathlib import Path
22
- from typing import ClassVar
22
+ from typing import Any, ClassVar
23
23
 
24
24
  import pymetadata
25
25
  from pymetadata.cache import (
@@ -37,34 +37,62 @@ logger = logging.getLogger(__name__)
37
37
 
38
38
  @dataclass
39
39
  class UnichemSource:
40
- """Unichem source.
41
-
42
- src_id (the src_id for this source),
43
- src_url (the main home page of the source),
44
- name (the unique name for the source in UniChem, always lower case),
45
- name_long (the full name of the source, as defined by the source),
46
- name_label (A name for the source suitable for use as a 'label' for the source within a web-page. Correct case setting for source, and always less than 30 characters),
47
- description (a description of the content of the source),
48
- base_id_url_available (an flag indicating whether this source provides a valid base_id_url for creating cpd-specific links [1=yes, 0=no]).
49
- base_id_url (the base url for constructing hyperlinks to this source [append an identifier from this source to the end of this url to create a valid url to a specific page for this cpd], unless aux_for_url=1),
50
- aux_for_url (A flag to indicate whether the aux_src field should be used to create hyperlinks instead of the src_compound_id [1=yes, 0=no]
40
+ """Database known to UniChem, see the `sources` endpoint of the web service.
41
+
42
+ Only `sourceID` and `name` are required. UniChem adds and removes the
43
+ descriptive fields without notice, so they are optional, and
44
+ `UnichemSource.from_dict` ignores fields which are not declared here.
45
+
46
+ Attributes:
47
+ sourceID: id of the source in UniChem
48
+ name: unique name of the source in UniChem, always lower case
49
+ nameLabel: name suitable as label of the source, in the case used by
50
+ the source
51
+ nameLong: full name of the source, as defined by the source
52
+ srcUrl: home page of the source
53
+ baseIdUrl: base url for links to a compound in the source; the
54
+ identifier of the compound is appended or replaces `{$id}`
55
+ description: description of the content of the source
56
+ UCICount: number of UniChem compound identifiers in the source
57
+ created: date the source was added to UniChem
58
+ lastChecked: date UniChem checked the source for an update
59
+ srcLastUpdated: date of the last update of the source
60
+ srcDetails: details on the source
61
+ updateComments: comments on the update of the source
62
+ private: the source is not public
51
63
  """
52
64
 
53
65
  sourceID: int
54
- srcUrl: str
55
66
  name: str
56
- nameLabel: str = field(repr=False)
57
- nameLong: str = field(repr=False)
58
- UCICount: int = field(repr=False)
59
- baseIdUrl: str = field(repr=False)
60
- description: str = field(repr=False)
61
- created: str = field(repr=False)
62
- lastUpdated: str = field(repr=False)
63
- srcDetails: str = field(repr=False)
64
- srcReleaseDate: str = field(repr=False)
65
- srcReleaseNumber: int = field(repr=False)
66
- updateComments: str = field(repr=False)
67
- private: bool = field(repr=False)
67
+ nameLabel: str | None = field(default=None, repr=False)
68
+ nameLong: str | None = field(default=None, repr=False)
69
+ srcUrl: str | None = None
70
+ baseIdUrl: str | None = field(default=None, repr=False)
71
+ description: str | None = field(default=None, repr=False)
72
+ UCICount: int | None = field(default=None, repr=False)
73
+ created: str | None = field(default=None, repr=False)
74
+ lastChecked: str | None = field(default=None, repr=False)
75
+ srcLastUpdated: str | None = field(default=None, repr=False)
76
+ srcDetails: str | None = field(default=None, repr=False)
77
+ updateComments: str | None = field(default=None, repr=False)
78
+ private: bool | None = field(default=None, repr=False)
79
+
80
+ @classmethod
81
+ def from_dict(cls, data: dict[str, Any]) -> "UnichemSource":
82
+ """Create a source from a source of the web service or of the cache.
83
+
84
+ Args:
85
+ data: fields of the source, fields unknown to `UnichemSource` are
86
+ ignored
87
+
88
+ Returns:
89
+ The source.
90
+ """
91
+ names = {f.name for f in fields(cls)}
92
+ unknown = sorted(set(data) - names)
93
+ if unknown:
94
+ logger.debug("Ignored fields of UniChem source: %s", unknown)
95
+ return cls(**{k: v for k, v in data.items() if k in names})
68
96
 
69
97
 
70
98
  class UnichemQuery:
@@ -117,7 +145,7 @@ class UnichemQuery:
117
145
  data = read_json_cache(
118
146
  unichem_sources_path, max_age=CACHE_DURATION_ONTOLOGY
119
147
  )
120
- return {int(k): UnichemSource(**v) for k, v in data.items()}
148
+ return {int(k): UnichemSource.from_dict(v) for k, v in data.items()}
121
149
 
122
150
  url = "https://www.ebi.ac.uk/unichem/api/v1/sources/"
123
151
  try:
@@ -131,11 +159,13 @@ class UnichemQuery:
131
159
  unichem_sources_path, reason=str(err)
132
160
  )
133
161
  if fallback is not None:
134
- return {int(k): UnichemSource(**v) for k, v in fallback.items()}
162
+ return {
163
+ int(k): UnichemSource.from_dict(v) for k, v in fallback.items()
164
+ }
135
165
  raise
136
166
 
137
167
  sources_list: list[UnichemSource] = [
138
- UnichemSource(**v) for v in data["sources"]
168
+ UnichemSource.from_dict(v) for v in data["sources"]
139
169
  ]
140
170
  sources = {source.sourceID: source for source in sources_list}
141
171
 
@@ -219,9 +249,6 @@ class UnichemQuery:
219
249
  accession = item["src_compound_id"]
220
250
  if source.baseIdUrl:
221
251
  # create and clean url
222
- if not source.baseIdUrl:
223
- continue
224
-
225
252
  url = f"{source.baseIdUrl}{accession}"
226
253
 
227
254
  url_accession = urllib.parse.quote(accession)
@@ -227,3 +227,82 @@ def test_resource_normalized(resource: str, expected: str) -> None:
227
227
  """Test normalization to identifiers.org compact identifiers."""
228
228
  a = RDFAnnotation(qualifier=BQB.IS, resource=resource)
229
229
  assert a.resource_normalized == expected
230
+
231
+
232
+ resource_kept_data = [
233
+ # collection which is not in the registry (see #81)
234
+ ("http://identifiers.org/sabiork/1406", "https://identifiers.org/sabiork/1406"),
235
+ ("urn:miriam:foo:bar", "https://identifiers.org/foo/bar"),
236
+ ("foo/bar", "https://identifiers.org/foo/bar"),
237
+ # collection which is not in the registry, term with a prefix of its own
238
+ (
239
+ "http://identifiers.org/unit/UO:0000040",
240
+ "https://identifiers.org/unit/UO:0000040",
241
+ ),
242
+ # compact identifier of a prefix which is not in the registry
243
+ ("https://identifiers.org/CMO:0000012", "https://identifiers.org/CMO:0000012"),
244
+ ("CMO:0000012", "https://identifiers.org/CMO:0000012"),
245
+ # term which does not match the pattern of its collection
246
+ (
247
+ "http://identifiers.org/chebi/000000035",
248
+ "https://identifiers.org/chebi/000000035",
249
+ ),
250
+ ("taxonomy/abc", "https://identifiers.org/taxonomy:abc"),
251
+ # prefix with a hyphen or an underscore
252
+ ("urn:miriam:ec-code:1.1.1.1", "https://identifiers.org/ec-code:1.1.1.1"),
253
+ (
254
+ "https://identifiers.org/ec-code/1.1.1.1",
255
+ "https://identifiers.org/ec-code:1.1.1.1",
256
+ ),
257
+ ("go_ref/GO_REF:0000041", "https://identifiers.org/GO_REF:0000041"),
258
+ (
259
+ "https://identifiers.org/GO_REF:0000041",
260
+ "https://identifiers.org/GO_REF:0000041",
261
+ ),
262
+ # prefixes are case insensitive
263
+ ("urn:miriam:CHEBI:CHEBI%3A33699", "https://identifiers.org/CHEBI:33699"),
264
+ ("TAXONOMY/9606", "https://identifiers.org/taxonomy:9606"),
265
+ ]
266
+
267
+
268
+ def assert_resource_kept(resource: str) -> None:
269
+ """Assert that the normalized resource is a url with the same meaning."""
270
+ a = RDFAnnotation(qualifier=BQB.IS, resource=resource, validate=False)
271
+ normalized = a.resource_normalized
272
+ assert normalized
273
+ assert normalized.startswith("https://")
274
+
275
+ b = RDFAnnotation(qualifier=BQB.IS, resource=normalized, validate=False)
276
+ assert (b.collection, b.term) == (a.collection, a.term)
277
+ assert b.resource_normalized == normalized
278
+
279
+
280
+ @pytest.mark.parametrize("resource,expected", resource_kept_data)
281
+ def test_resource_normalized_keeps_resource(resource: str, expected: str) -> None:
282
+ """Test that collection and term survive the normalization, see #81."""
283
+ a = RDFAnnotation(qualifier=BQB.IS, resource=resource, validate=False)
284
+ assert a.resource_normalized == expected
285
+ assert_resource_kept(resource)
286
+
287
+
288
+ @pytest.mark.parametrize(
289
+ "resource", [resource for resource, _ in resource_normalized_data]
290
+ )
291
+ def test_resource_normalized_roundtrip(resource: str) -> None:
292
+ """Test that the normalized resource parses to the same annotation."""
293
+ assert_resource_kept(resource)
294
+
295
+
296
+ def test_resource_normalized_registry() -> None:
297
+ """Test the normalization with the sample of every registry namespace."""
298
+ from pymetadata.webservices.registry import get_registry
299
+
300
+ for prefix, namespace in get_registry().ns_dict.items():
301
+ sample_id = namespace.sampleId
302
+ if not sample_id:
303
+ continue
304
+ for resource in [
305
+ f"https://identifiers.org/{prefix}/{sample_id}",
306
+ f"urn:miriam:{prefix}:{sample_id.replace(':', '%3A')}",
307
+ ]:
308
+ assert_resource_kept(resource)