pymetadata 0.7.0__tar.gz → 0.8.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 (229) hide show
  1. {pymetadata-0.7.0 → pymetadata-0.8.0}/.bumpversion.toml +1 -1
  2. {pymetadata-0.7.0 → pymetadata-0.8.0}/CITATION.cff +1 -1
  3. {pymetadata-0.7.0 → pymetadata-0.8.0}/CLAUDE.md +9 -4
  4. {pymetadata-0.7.0 → pymetadata-0.8.0}/PKG-INFO +1 -1
  5. {pymetadata-0.7.0 → pymetadata-0.8.0}/docs/annotations.md +8 -2
  6. {pymetadata-0.7.0 → pymetadata-0.8.0}/docs/api/index.md +2 -1
  7. pymetadata-0.8.0/docs/api/webservices.uniprot.md +3 -0
  8. pymetadata-0.8.0/release-notes/0.8.0.md +19 -0
  9. {pymetadata-0.7.0 → pymetadata-0.8.0}/src/pymetadata/__init__.py +1 -1
  10. {pymetadata-0.7.0 → pymetadata-0.8.0}/src/pymetadata/core/annotation.py +100 -5
  11. {pymetadata-0.7.0 → pymetadata-0.8.0}/src/pymetadata/webservices/chebi.py +78 -3
  12. {pymetadata-0.7.0 → pymetadata-0.8.0}/src/pymetadata/webservices/ols.py +35 -7
  13. pymetadata-0.8.0/src/pymetadata/webservices/uniprot.py +140 -0
  14. {pymetadata-0.7.0 → pymetadata-0.8.0}/src/pymetadata/webservices/webservice.py +41 -0
  15. {pymetadata-0.7.0 → pymetadata-0.8.0}/tests/core/test_annotation.py +202 -6
  16. {pymetadata-0.7.0 → pymetadata-0.8.0}/tests/test_annotation_resolution.py +1 -1
  17. {pymetadata-0.7.0 → pymetadata-0.8.0}/tests/test_chebi.py +70 -0
  18. pymetadata-0.8.0/tests/test_ols.py +148 -0
  19. pymetadata-0.8.0/tests/test_uniprot.py +129 -0
  20. pymetadata-0.8.0/tests/test_webservice.py +95 -0
  21. {pymetadata-0.7.0 → pymetadata-0.8.0}/zensical.toml +1 -0
  22. pymetadata-0.7.0/tests/test_ols.py +0 -71
  23. pymetadata-0.7.0/tests/test_webservice.py +0 -40
  24. {pymetadata-0.7.0 → pymetadata-0.8.0}/.github/CODEOWNERS +0 -0
  25. {pymetadata-0.7.0 → pymetadata-0.8.0}/.github/dependabot.yml +0 -0
  26. {pymetadata-0.7.0 → pymetadata-0.8.0}/.github/pull_request_template.md +0 -0
  27. {pymetadata-0.7.0 → pymetadata-0.8.0}/.github/rulesets/apply.sh +0 -0
  28. {pymetadata-0.7.0 → pymetadata-0.8.0}/.github/rulesets/develop.json +0 -0
  29. {pymetadata-0.7.0 → pymetadata-0.8.0}/.github/rulesets/main.json +0 -0
  30. {pymetadata-0.7.0 → pymetadata-0.8.0}/.github/rulesets/tags.json +0 -0
  31. {pymetadata-0.7.0 → pymetadata-0.8.0}/.github/workflows/ci-cd.yml +0 -0
  32. {pymetadata-0.7.0 → pymetadata-0.8.0}/.github/workflows/docs.yml +0 -0
  33. {pymetadata-0.7.0 → pymetadata-0.8.0}/.github/workflows/ruff.yml +0 -0
  34. {pymetadata-0.7.0 → pymetadata-0.8.0}/.github/workflows/ty.yml +0 -0
  35. {pymetadata-0.7.0 → pymetadata-0.8.0}/.gitignore +0 -0
  36. {pymetadata-0.7.0 → pymetadata-0.8.0}/.pre-commit-config.yaml +0 -0
  37. {pymetadata-0.7.0 → pymetadata-0.8.0}/.python-version +0 -0
  38. {pymetadata-0.7.0 → pymetadata-0.8.0}/.ruff.toml +0 -0
  39. {pymetadata-0.7.0 → pymetadata-0.8.0}/.zenodo.json +0 -0
  40. {pymetadata-0.7.0 → pymetadata-0.8.0}/LICENSE +0 -0
  41. {pymetadata-0.7.0 → pymetadata-0.8.0}/README.md +0 -0
  42. {pymetadata-0.7.0 → pymetadata-0.8.0}/docs/README.md +0 -0
  43. {pymetadata-0.7.0 → pymetadata-0.8.0}/docs/api/cache.md +0 -0
  44. {pymetadata-0.7.0 → pymetadata-0.8.0}/docs/api/console.md +0 -0
  45. {pymetadata-0.7.0 → pymetadata-0.8.0}/docs/api/core.annotation.md +0 -0
  46. {pymetadata-0.7.0 → pymetadata-0.8.0}/docs/api/core.creator.md +0 -0
  47. {pymetadata-0.7.0 → pymetadata-0.8.0}/docs/api/core.miriam.md +0 -0
  48. {pymetadata-0.7.0 → pymetadata-0.8.0}/docs/api/core.xref.md +0 -0
  49. {pymetadata-0.7.0 → pymetadata-0.8.0}/docs/api/log.md +0 -0
  50. {pymetadata-0.7.0 → pymetadata-0.8.0}/docs/api/omex.md +0 -0
  51. {pymetadata-0.7.0 → pymetadata-0.8.0}/docs/api/ontologies.term.md +0 -0
  52. {pymetadata-0.7.0 → pymetadata-0.8.0}/docs/api/webservices.chebi.md +0 -0
  53. {pymetadata-0.7.0 → pymetadata-0.8.0}/docs/api/webservices.ols.md +0 -0
  54. {pymetadata-0.7.0 → pymetadata-0.8.0}/docs/api/webservices.registry.md +0 -0
  55. {pymetadata-0.7.0 → pymetadata-0.8.0}/docs/api/webservices.unichem.md +0 -0
  56. {pymetadata-0.7.0 → pymetadata-0.8.0}/docs/api/webservices.webservice.md +0 -0
  57. {pymetadata-0.7.0 → pymetadata-0.8.0}/docs/development.md +0 -0
  58. {pymetadata-0.7.0 → pymetadata-0.8.0}/docs/images/HARMONY2026_slides.png +0 -0
  59. {pymetadata-0.7.0 → pymetadata-0.8.0}/docs/images/favicon/about.txt +0 -0
  60. {pymetadata-0.7.0 → pymetadata-0.8.0}/docs/images/favicon/android-chrome-192x192.png +0 -0
  61. {pymetadata-0.7.0 → pymetadata-0.8.0}/docs/images/favicon/android-chrome-512x512.png +0 -0
  62. {pymetadata-0.7.0 → pymetadata-0.8.0}/docs/images/favicon/apple-touch-icon.png +0 -0
  63. {pymetadata-0.7.0 → pymetadata-0.8.0}/docs/images/favicon/favicon-16x16.png +0 -0
  64. {pymetadata-0.7.0 → pymetadata-0.8.0}/docs/images/favicon/favicon-32x32.png +0 -0
  65. {pymetadata-0.7.0 → pymetadata-0.8.0}/docs/images/favicon/favicon.ico +0 -0
  66. {pymetadata-0.7.0 → pymetadata-0.8.0}/docs/images/favicon/favicon_io.zip +0 -0
  67. {pymetadata-0.7.0 → pymetadata-0.8.0}/docs/images/favicon/pymetadata-100x100-300dpi.png +0 -0
  68. {pymetadata-0.7.0 → pymetadata-0.8.0}/docs/images/favicon/site.webmanifest +0 -0
  69. {pymetadata-0.7.0 → pymetadata-0.8.0}/docs/index.md +0 -0
  70. {pymetadata-0.7.0 → pymetadata-0.8.0}/docs/installation.md +0 -0
  71. {pymetadata-0.7.0 → pymetadata-0.8.0}/docs/omex.md +0 -0
  72. {pymetadata-0.7.0 → pymetadata-0.8.0}/docs/robots.txt +0 -0
  73. {pymetadata-0.7.0 → pymetadata-0.8.0}/examples/annotation.py +0 -0
  74. {pymetadata-0.7.0 → pymetadata-0.8.0}/examples/cache_path.py +0 -0
  75. {pymetadata-0.7.0 → pymetadata-0.8.0}/examples/omex/biomodels_omex/MODEL2402290004.4_pymetadata.omex +0 -0
  76. {pymetadata-0.7.0 → pymetadata-0.8.0}/examples/omex/biomodels_omex/create_omex.py +0 -0
  77. {pymetadata-0.7.0 → pymetadata-0.8.0}/examples/omex/biomodels_omex/files/BioModelsMetadata_MODEL2402290004.csv +0 -0
  78. {pymetadata-0.7.0 → pymetadata-0.8.0}/examples/omex/biomodels_omex/files/EndoAcuteBZ1.m +0 -0
  79. {pymetadata-0.7.0 → pymetadata-0.8.0}/examples/omex/biomodels_omex/files/EndoAcuteBZ2.m +0 -0
  80. {pymetadata-0.7.0 → pymetadata-0.8.0}/examples/omex/biomodels_omex/files/EndoAcuteBZ3.m +0 -0
  81. {pymetadata-0.7.0 → pymetadata-0.8.0}/examples/omex/biomodels_omex/files/EndoChronicBZ.m +0 -0
  82. {pymetadata-0.7.0 → pymetadata-0.8.0}/examples/omex/biomodels_omex/files/EndoChronicRZ1.m +0 -0
  83. {pymetadata-0.7.0 → pymetadata-0.8.0}/examples/omex/biomodels_omex/files/EndoChronicRZ2.m +0 -0
  84. {pymetadata-0.7.0 → pymetadata-0.8.0}/examples/omex/biomodels_omex/files/EndoNZ.m +0 -0
  85. {pymetadata-0.7.0 → pymetadata-0.8.0}/examples/omex/biomodels_omex/files/POM253.mat +0 -0
  86. {pymetadata-0.7.0 → pymetadata-0.8.0}/examples/omex/biomodels_omex/files/PlotAPCaTTaAcuteAndChronic.m +0 -0
  87. {pymetadata-0.7.0 → pymetadata-0.8.0}/examples/omex/biomodels_omex/files/Zhou2024_Updated-model.m +0 -0
  88. {pymetadata-0.7.0 → pymetadata-0.8.0}/examples/omex/biomodels_omex/files/getCurrentsStructureCaMKSKJrelpKinetics.m +0 -0
  89. {pymetadata-0.7.0 → pymetadata-0.8.0}/examples/omex/biomodels_omex/files/getStartingState.m +0 -0
  90. {pymetadata-0.7.0 → pymetadata-0.8.0}/examples/omex/biomodels_omex/files/main.m +0 -0
  91. {pymetadata-0.7.0 → pymetadata-0.8.0}/examples/omex/biomodels_omex/files/manifest.xml +0 -0
  92. {pymetadata-0.7.0 → pymetadata-0.8.0}/examples/omex/biomodels_omex/files/metadata.rdf +0 -0
  93. {pymetadata-0.7.0 → pymetadata-0.8.0}/examples/omex/biomodels_omex/files/modelRunnerCaMKSKJrelpKinetics1ms.m +0 -0
  94. {pymetadata-0.7.0 → pymetadata-0.8.0}/examples/omex/biomodels_omex/files/model_ToRORd_LandCaMKSKJrelpKinetics.m +0 -0
  95. {pymetadata-0.7.0 → pymetadata-0.8.0}/examples/omex/omex.py +0 -0
  96. {pymetadata-0.7.0 → pymetadata-0.8.0}/examples/omex/omex_from_url.py +0 -0
  97. {pymetadata-0.7.0 → pymetadata-0.8.0}/examples/omex/test.omex +0 -0
  98. {pymetadata-0.7.0 → pymetadata-0.8.0}/examples/pbpko.py +0 -0
  99. {pymetadata-0.7.0 → pymetadata-0.8.0}/examples/sbo.py +0 -0
  100. {pymetadata-0.7.0 → pymetadata-0.8.0}/presentations/HARMONY2026/.gitignore +0 -0
  101. {pymetadata-0.7.0 → pymetadata-0.8.0}/presentations/HARMONY2026/README.md +0 -0
  102. {pymetadata-0.7.0 → pymetadata-0.8.0}/presentations/HARMONY2026/_extensions/grantmcdermott/clean/_extension.yml +0 -0
  103. {pymetadata-0.7.0 → pymetadata-0.8.0}/presentations/HARMONY2026/_extensions/grantmcdermott/clean/clean.scss +0 -0
  104. {pymetadata-0.7.0 → pymetadata-0.8.0}/presentations/HARMONY2026/_extensions/grantmcdermott/clean/mathjax-config.js +0 -0
  105. {pymetadata-0.7.0 → pymetadata-0.8.0}/presentations/HARMONY2026/custom.scss +0 -0
  106. {pymetadata-0.7.0 → pymetadata-0.8.0}/presentations/HARMONY2026/images/annotation_example.png +0 -0
  107. {pymetadata-0.7.0 → pymetadata-0.8.0}/presentations/HARMONY2026/images/annotation_resolved.png +0 -0
  108. {pymetadata-0.7.0 → pymetadata-0.8.0}/presentations/HARMONY2026/images/annotation_resolved_small.png +0 -0
  109. {pymetadata-0.7.0 → pymetadata-0.8.0}/presentations/HARMONY2026/images/github_screenshot.png +0 -0
  110. {pymetadata-0.7.0 → pymetadata-0.8.0}/presentations/HARMONY2026/images/headshot.png +0 -0
  111. {pymetadata-0.7.0 → pymetadata-0.8.0}/presentations/HARMONY2026/images/omex_logo.png +0 -0
  112. {pymetadata-0.7.0 → pymetadata-0.8.0}/presentations/HARMONY2026/images/omex_overview.png +0 -0
  113. {pymetadata-0.7.0 → pymetadata-0.8.0}/presentations/HARMONY2026/images/pymetadata.png +0 -0
  114. {pymetadata-0.7.0 → pymetadata-0.8.0}/presentations/HARMONY2026/images/pymetadata_large.png +0 -0
  115. {pymetadata-0.7.0 → pymetadata-0.8.0}/presentations/HARMONY2026/images/qr_livermetabolism.png +0 -0
  116. {pymetadata-0.7.0 → pymetadata-0.8.0}/presentations/HARMONY2026/mathjax-config.js +0 -0
  117. {pymetadata-0.7.0 → pymetadata-0.8.0}/presentations/HARMONY2026/pymetadata.bib +0 -0
  118. {pymetadata-0.7.0 → pymetadata-0.8.0}/presentations/HARMONY2026/pymetadata.qmd +0 -0
  119. {pymetadata-0.7.0 → pymetadata-0.8.0}/presentations/HARMONY2026/styles.css +0 -0
  120. {pymetadata-0.7.0 → pymetadata-0.8.0}/pyproject.toml +0 -0
  121. {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.0.10.md +0 -0
  122. {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.0.11.md +0 -0
  123. {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.0.12.md +0 -0
  124. {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.0.13.md +0 -0
  125. {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.0.14.md +0 -0
  126. {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.0.15.md +0 -0
  127. {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.0.16.md +0 -0
  128. {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.0.17.md +0 -0
  129. {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.0.18.md +0 -0
  130. {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.0.19.md +0 -0
  131. {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.0.2.md +0 -0
  132. {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.0.20.md +0 -0
  133. {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.0.21.md +0 -0
  134. {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.0.22.md +0 -0
  135. {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.0.3.md +0 -0
  136. {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.0.4.md +0 -0
  137. {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.0.5.md +0 -0
  138. {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.0.6.md +0 -0
  139. {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.0.7.md +0 -0
  140. {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.0.8.md +0 -0
  141. {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.0.9.md +0 -0
  142. {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.1.0.md +0 -0
  143. {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.2.1.md +0 -0
  144. {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.2.10.md +0 -0
  145. {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.2.2.md +0 -0
  146. {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.2.3.md +0 -0
  147. {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.2.4.md +0 -0
  148. {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.2.5.md +0 -0
  149. {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.2.6.md +0 -0
  150. {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.2.7.md +0 -0
  151. {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.2.8.md +0 -0
  152. {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.2.9.md +0 -0
  153. {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.3.0.md +0 -0
  154. {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.3.1.md +0 -0
  155. {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.3.10.md +0 -0
  156. {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.3.11.md +0 -0
  157. {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.3.2.md +0 -0
  158. {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.3.3.md +0 -0
  159. {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.3.4.md +0 -0
  160. {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.3.5.md +0 -0
  161. {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.3.6.md +0 -0
  162. {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.3.7.md +0 -0
  163. {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.3.8.md +0 -0
  164. {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.3.9.md +0 -0
  165. {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.4.0.md +0 -0
  166. {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.4.1.md +0 -0
  167. {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.4.2.md +0 -0
  168. {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.4.3.md +0 -0
  169. {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.4.4.md +0 -0
  170. {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.5.0.md +0 -0
  171. {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.5.1.md +0 -0
  172. {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.5.10.md +0 -0
  173. {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.5.11.md +0 -0
  174. {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.5.12.md +0 -0
  175. {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.5.2.md +0 -0
  176. {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.5.3.md +0 -0
  177. {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.5.4.md +0 -0
  178. {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.5.5.md +0 -0
  179. {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.5.6.md +0 -0
  180. {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.5.7.md +0 -0
  181. {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.5.8.md +0 -0
  182. {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.5.9.md +0 -0
  183. {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.6.0.md +0 -0
  184. {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.6.1.md +0 -0
  185. {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.6.2.md +0 -0
  186. {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.6.3.md +0 -0
  187. {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.6.4.md +0 -0
  188. {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.6.5.md +0 -0
  189. {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.6.6.md +0 -0
  190. {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.7.0.md +0 -0
  191. {pymetadata-0.7.0 → pymetadata-0.8.0}/scripts/llms_txt.py +0 -0
  192. {pymetadata-0.7.0 → pymetadata-0.8.0}/src/pymetadata/_files.py +0 -0
  193. {pymetadata-0.7.0 → pymetadata-0.8.0}/src/pymetadata/cache.py +0 -0
  194. {pymetadata-0.7.0 → pymetadata-0.8.0}/src/pymetadata/console.py +0 -0
  195. {pymetadata-0.7.0 → pymetadata-0.8.0}/src/pymetadata/core/__init__.py +0 -0
  196. {pymetadata-0.7.0 → pymetadata-0.8.0}/src/pymetadata/core/creator.py +0 -0
  197. {pymetadata-0.7.0 → pymetadata-0.8.0}/src/pymetadata/core/miriam.py +0 -0
  198. {pymetadata-0.7.0 → pymetadata-0.8.0}/src/pymetadata/core/synonym.py +0 -0
  199. {pymetadata-0.7.0 → pymetadata-0.8.0}/src/pymetadata/core/xref.py +0 -0
  200. {pymetadata-0.7.0 → pymetadata-0.8.0}/src/pymetadata/log.py +0 -0
  201. {pymetadata-0.7.0 → pymetadata-0.8.0}/src/pymetadata/omex.py +0 -0
  202. {pymetadata-0.7.0 → pymetadata-0.8.0}/src/pymetadata/ontologies/__init__.py +0 -0
  203. {pymetadata-0.7.0 → pymetadata-0.8.0}/src/pymetadata/ontologies/_ontology_builder.py +0 -0
  204. {pymetadata-0.7.0 → pymetadata-0.8.0}/src/pymetadata/ontologies/kisao.py +0 -0
  205. {pymetadata-0.7.0 → pymetadata-0.8.0}/src/pymetadata/ontologies/pbpko.py +0 -0
  206. {pymetadata-0.7.0 → pymetadata-0.8.0}/src/pymetadata/ontologies/sbo.py +0 -0
  207. {pymetadata-0.7.0 → pymetadata-0.8.0}/src/pymetadata/ontologies/term.py +0 -0
  208. {pymetadata-0.7.0 → pymetadata-0.8.0}/src/pymetadata/py.typed +0 -0
  209. {pymetadata-0.7.0 → pymetadata-0.8.0}/src/pymetadata/resources/ontologies/README.md +0 -0
  210. {pymetadata-0.7.0 → pymetadata-0.8.0}/src/pymetadata/webservices/__init__.py +0 -0
  211. {pymetadata-0.7.0 → pymetadata-0.8.0}/src/pymetadata/webservices/registry.py +0 -0
  212. {pymetadata-0.7.0 → pymetadata-0.8.0}/src/pymetadata/webservices/unichem.py +0 -0
  213. {pymetadata-0.7.0 → pymetadata-0.8.0}/tests/core/test_creator.py +0 -0
  214. {pymetadata-0.7.0 → pymetadata-0.8.0}/tests/data/omex/BIOMD0000000001.omex +0 -0
  215. {pymetadata-0.7.0 → pymetadata-0.8.0}/tests/data/omex/CombineArchiveShowCase.omex +0 -0
  216. {pymetadata-0.7.0 → pymetadata-0.8.0}/tests/data/omex/CombineArchiveShowCase_manifest.xml +0 -0
  217. {pymetadata-0.7.0 → pymetadata-0.8.0}/tests/data/omex/CompModels.omex +0 -0
  218. {pymetadata-0.7.0 → pymetadata-0.8.0}/tests/data/omex/CompModels_manifest.xml +0 -0
  219. {pymetadata-0.7.0 → pymetadata-0.8.0}/tests/data/omex/iCGB21FR.omex +0 -0
  220. {pymetadata-0.7.0 → pymetadata-0.8.0}/tests/test_cache.py +0 -0
  221. {pymetadata-0.7.0 → pymetadata-0.8.0}/tests/test_log.py +0 -0
  222. {pymetadata-0.7.0 → pymetadata-0.8.0}/tests/test_offline.py +0 -0
  223. {pymetadata-0.7.0 → pymetadata-0.8.0}/tests/test_omex.py +0 -0
  224. {pymetadata-0.7.0 → pymetadata-0.8.0}/tests/test_ontology.py +0 -0
  225. {pymetadata-0.7.0 → pymetadata-0.8.0}/tests/test_ontology_builder.py +0 -0
  226. {pymetadata-0.7.0 → pymetadata-0.8.0}/tests/test_registry.py +0 -0
  227. {pymetadata-0.7.0 → pymetadata-0.8.0}/tests/test_sbo_kisao.py +0 -0
  228. {pymetadata-0.7.0 → pymetadata-0.8.0}/tests/test_unichem.py +0 -0
  229. {pymetadata-0.7.0 → pymetadata-0.8.0}/tox.ini +0 -0
@@ -1,5 +1,5 @@
1
1
  [tool.bumpversion]
2
- current_version = "0.7.0"
2
+ current_version = "0.8.0"
3
3
  commit = true
4
4
  parse = "(?P<major>\\d+)\\.(?P<minor>\\d+)\\.(?P<patch>\\d+)"
5
5
  serialize = ["{major}.{minor}.{patch}"]
@@ -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.7.0"
11
+ version: "0.8.0"
12
12
  date-released: "2026-09-23"
13
13
  doi: "10.5281/zenodo.5308801"
14
14
  license: MIT
@@ -73,7 +73,10 @@ identifiers.org URL (classic or compact), a bioregistry.io URL, a `urn:miriam:*`
73
73
  a `collection/term` shorthand, or an arbitrary URL, and normalizes it into
74
74
  `collection` + `term`. Validation checks the term against the identifiers.org
75
75
  registry pattern (`webservices/registry.py`). `RDFAnnotationData` enriches an
76
- annotation with label/description/synonyms/xrefs by querying OLS.
76
+ annotation with label/description/synonyms/xrefs by querying OLS, and with the
77
+ providers of the collection: `url` is the url of the primary provider (the official
78
+ non deprecated one, see `primary_resource`), `providers` lists the others, and
79
+ `collection_name`, `collection_homepage` and `pattern_match` describe the collection.
77
80
 
78
81
  **Ontologies are generated code.** `ontologies/sbo.py`, `kisao.py`, `pbpko.py`
79
82
  are machine-generated (large; do not hand-edit). Each is a class of
@@ -103,9 +106,11 @@ cheap (the `pymetadata.metadata` package was removed in 0.6.0).
103
106
 
104
107
  **Web services + caching.** The `webservices/` package holds everything which
105
108
  hits a remote API: `ols.py` (EBI OLS4), `registry.py` (identifiers.org),
106
- `chebi.py` and `unichem.py`, all going through the shared, retrying session of
109
+ `chebi.py` (also the svg structure of a compound), `uniprot.py` and `unichem.py`, all going through the shared, retrying session of
107
110
  `webservices/webservice.py`, whose `get_json` turns an unreachable service, an
108
- error response and a non-JSON body into one `WebserviceError`. They share the
111
+ error response and a non-JSON body into one `WebserviceError`; a 404 raises its
112
+ subclass `WebserviceNotFoundError`, which callers treat as an unknown entry and
113
+ never answer from an outdated cache. `get_bytes` does the same for svg images. They share the
109
114
  JSON cache helpers in `cache.py`, gated by the module-level globals
110
115
  `pymetadata.CACHE_USE` (default `True`) and `pymetadata.CACHE_PATH`
111
116
  (`~/.cache/pymetadata`). These are read at call time, so consumers override them by
@@ -115,7 +120,7 @@ an ontology release) or `CACHE_DURATION_REGISTRY` (24 h, for the identifiers.org
115
120
  registry). When a refresh fails, `read_json_cache_fallback` returns the outdated
116
121
  content with a warning instead of failing, so queries answered before keep
117
122
  working offline; `tests/test_offline.py` covers this per service. The corresponding tests
118
- (`test_ols.py`, `test_registry.py`, `test_chebi.py`, `test_unichem.py`) require
123
+ (`test_ols.py`, `test_registry.py`, `test_chebi.py`, `test_unichem.py`, `test_uniprot.py`) require
119
124
  network access.
120
125
 
121
126
  `console.py` (rich console, for scripts and `__main__` blocks) and `log.py`
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: pymetadata
3
- Version: 0.7.0
3
+ Version: 0.8.0
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
@@ -90,11 +90,15 @@ from pymetadata.core.miriam import BQB
90
90
 
91
91
  annotation = RDFAnnotation(qualifier=BQB.IS, resource="chebi/CHEBI:33699")
92
92
  data = RDFAnnotationData(annotation)
93
- print(data.url) # url of the first provider
93
+ print(data.url) # url of the primary provider, the official one of the registry
94
+ print(data.providers) # the non deprecated providers, the primary one first
95
+ print(data.collection_name) # name of the collection
96
+ print(data.collection_homepage) # homepage of the primary provider
97
+ print(data.pattern_match) # whether the term matches the pattern of the collection
94
98
  print(data.xrefs) # one entry per identifiers.org provider
95
99
  ```
96
100
 
97
- `query_ols()` then asks the [Ontology Lookup Service](https://www.ebi.ac.uk/ols4) for the term itself and fills in label, description and synonyms. Note that it also replaces `xrefs` with the cross references reported by OLS, which can be empty for a given term:
101
+ `query_ols()` then asks the [Ontology Lookup Service](https://www.ebi.ac.uk/ols4) for the term itself and fills in label, description and synonyms, and the ontology, the IRI and the page of the term in OLS (`ontology`, `iri`, `ols_url`). A collection which is not on OLS gives no information and no message, a term which OLS does not know gives a warning. Note that it also replaces `xrefs` with the cross references reported by OLS, which can be empty for a given term:
98
102
 
99
103
  ```python
100
104
  data.query_ols()
@@ -104,6 +108,8 @@ print(data.description) # An RNA molecule that transfers the coding information
104
108
  print(data.synonyms) # mRNA, ...
105
109
  ```
106
110
 
111
+ Protein information of UniProt and the structure of a ChEBI compound are queried with `UniprotQuery.query("P69905")` and `ChebiQuery.structure("CHEBI:15377")`, see the [API reference](api/webservices.uniprot.md).
112
+
107
113
  Both steps require network access. Responses are cached on disk by default, so repeated terms are not queried again; see [Cache](installation.md#cache).
108
114
 
109
115
  ## Ontology terms { #ontology-terms }
@@ -30,10 +30,11 @@ The services queried for the information which does not ship with the package. T
30
30
 
31
31
  | module | description |
32
32
  | --- | --- |
33
- | [webservices.chebi](webservices.chebi.md) | Substance information from ChEBI |
33
+ | [webservices.chebi](webservices.chebi.md) | Substance information and structure from ChEBI |
34
34
  | [webservices.ols](webservices.ols.md) | Lookup of ontology terms in the Ontology Lookup Service |
35
35
  | [webservices.registry](webservices.registry.md) | The identifiers.org registry |
36
36
  | [webservices.unichem](webservices.unichem.md) | Substance cross references from UniChem |
37
+ | [webservices.uniprot](webservices.uniprot.md) | Protein information from UniProt |
37
38
  | [webservices.webservice](webservices.webservice.md) | The shared HTTP session |
38
39
 
39
40
  ## pymetadata.ontologies
@@ -0,0 +1,3 @@
1
+ # webservices.uniprot
2
+
3
+ ::: pymetadata.webservices.uniprot
@@ -0,0 +1,19 @@
1
+ # Release notes for pymetadata 0.8.0
2
+ ![pymetadata](./docs/images/favicon/pymetadata-100x100-300dpi.png)
3
+
4
+ ## New
5
+
6
+ - `UniprotQuery.query` reads the protein information of a UniProt accession (name, organism, genes, length, function), including isoform accessions such as `P69905-2`. Responses are cached, an outdated cache is used when UniProt cannot be reached.
7
+ - `ChebiQuery.structure` returns the structure of a ChEBI compound as svg, cached and written atomically.
8
+ - `RDFAnnotationData` carries the `ontology`, `iri` and `ols_url` of the term in OLS, and reports the `providers` of a collection with the primary one first, the `collection_name` and `collection_homepage`, and `pattern_match`, whether the term matches the pattern of its collection.
9
+ - `OLSQuery` reports the `ontology`, the `iri` and the page of the term in OLS (`ols_url`).
10
+ - `WebserviceNotFoundError`, a subclass of `WebserviceError` for a 404 answer, and `get_bytes` for binary downloads.
11
+
12
+ ## Changes
13
+
14
+ - A collection which is not on OLS gives no information and no warning or error.
15
+ - A term which OLS does not know is a warning and no longer an error; an outdated cache entry is not used for it.
16
+ - The `url` of an annotation is the url of the official non deprecated provider of the registry, e.g., UniProt links UniProt instead of NCBI Protein.
17
+ - A corrupt cache file of the ChEBI and UniProt queries is queried again.
18
+
19
+ 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.7.0"
10
+ __version__ = "0.8.0"
11
11
 
12
12
 
13
13
  program_name: str = "pymetadata"
@@ -21,6 +21,7 @@ print(annotation.validate())
21
21
  import logging
22
22
  import re
23
23
  import urllib.parse
24
+ from dataclasses import asdict, dataclass
24
25
  from enum import Enum
25
26
  from functools import lru_cache
26
27
  from pprint import pprint
@@ -29,7 +30,7 @@ from typing import Any, ClassVar, Final
29
30
  from pymetadata.core.miriam import BQB, BQM
30
31
  from pymetadata.core.xref import CrossReference, is_url
31
32
  from pymetadata.webservices.ols import ONTOLOGIES, OLSQuery
32
- from pymetadata.webservices.registry import Namespace, get_registry
33
+ from pymetadata.webservices.registry import Namespace, Resource, get_registry
33
34
 
34
35
  _OLS_QUERY: OLSQuery | None = None
35
36
 
@@ -482,6 +483,46 @@ class RDFAnnotation:
482
483
  return valid_qualifier and valid_term
483
484
 
484
485
 
486
+ @dataclass
487
+ class Provider:
488
+ """A provider which resolves the term of an annotation.
489
+
490
+ Attributes:
491
+ name: name of the provider, e.g., `UniProt`
492
+ url: url of the term at the provider
493
+ official: whether the registry names it the official provider
494
+ """
495
+
496
+ name: str
497
+ url: str
498
+ official: bool
499
+
500
+
501
+ def primary_resource(resources: list[Resource]) -> Resource | None:
502
+ """Get the provider a term links to: the official non deprecated one.
503
+
504
+ Falls back to the first non deprecated provider, and to the first provider
505
+ when all are deprecated.
506
+ """
507
+ active = [resource for resource in resources if not resource.deprecated]
508
+ for resource in active:
509
+ if resource.official:
510
+ return resource
511
+ if active:
512
+ return active[0]
513
+ return resources[0] if resources else None
514
+
515
+
516
+ def pattern_matches(pattern: str | None, term: str | None) -> bool | None:
517
+ """Check a term against the pattern of its collection, None if it cannot be checked."""
518
+ if not pattern or not term:
519
+ return None
520
+ try:
521
+ return re.match(pattern, term) is not None
522
+ except re.error:
523
+ return None
524
+
525
+
485
526
  class RDFAnnotationData(RDFAnnotation):
486
527
  """An annotation with the information behind the identifier resolved.
487
528
 
@@ -492,8 +533,16 @@ class RDFAnnotationData(RDFAnnotation):
492
533
  cross references reported by OLS.
493
534
 
494
535
  Attributes:
495
- url: url of the first provider of the collection
536
+ url: url of the primary provider of the collection
537
+ providers: non deprecated providers, the primary one first
538
+ collection_name: name of the collection, e.g., `UniProt Knowledgebase`
539
+ collection_homepage: homepage of the primary provider
540
+ pattern_match: whether the term matches the pattern of the collection,
541
+ None if it cannot be checked
496
542
  label: name of the term
543
+ ontology: ontology id of the term in OLS, e.g., `go`
544
+ iri: IRI of the term
545
+ ols_url: page of the term in OLS
497
546
  description: definition of the term
498
547
  synonyms: synonyms of the term
499
548
  xrefs: cross references of the term
@@ -519,8 +568,15 @@ class RDFAnnotationData(RDFAnnotation):
519
568
  self.term: str | None = annotation.term
520
569
  self.provider = annotation.provider
521
570
  self.url: str | None = None
571
+ self.providers: list[Provider] = []
572
+ self.collection_name: str | None = None
573
+ self.collection_homepage: str | None = None
574
+ self.pattern_match: bool | None = None
522
575
  self.description: str | None = None
523
576
  self.label: str | None = None
577
+ self.ontology: str | None = None
578
+ self.iri: str | None = None
579
+ self.ols_url: str | None = None
524
580
  self.synonyms: list[Any] = []
525
581
  self.xrefs: list[Any] = []
526
582
  self.warnings: list[Any] = []
@@ -539,6 +595,7 @@ class RDFAnnotationData(RDFAnnotation):
539
595
  if not namespace.resources:
540
596
  namespace.resources = []
541
597
 
598
+ urls: dict[int, str] = {}
542
599
  for ns_resource in namespace.resources:
543
600
  # create url
544
601
  url = ns_resource.urlPattern
@@ -563,9 +620,7 @@ class RDFAnnotationData(RDFAnnotation):
563
620
  url = url.replace("{$Id}", term)
564
621
  url = url.replace("{$id}", term)
565
622
 
566
- if not self.url:
567
- # set url to first resource url
568
- self.url = url
623
+ urls[id(ns_resource)] = url
569
624
 
570
625
  # print(url)
571
626
  _xref = CrossReference(
@@ -575,6 +630,36 @@ class RDFAnnotationData(RDFAnnotation):
575
630
  if valid:
576
631
  self.xrefs.append(_xref)
577
632
 
633
+ primary = primary_resource(namespace.resources)
634
+ self.collection_name = namespace.name
635
+ candidates = [self.term]
636
+ if (
637
+ namespace_embedded
638
+ and namespace.prefix
639
+ and self.term
640
+ and not self.term.lower().startswith(f"{namespace.prefix.lower()}:")
641
+ ):
642
+ # the pattern contains the prefix, the term is written without it
643
+ candidates = [
644
+ f"{namespace.prefix.upper()}:{self.term}",
645
+ f"{namespace.prefix}:{self.term}",
646
+ ]
647
+ results = [pattern_matches(namespace.pattern, c) for c in candidates]
648
+ self.pattern_match = (
649
+ True if any(results) else (False if False in results else None)
650
+ )
651
+ if primary is not None:
652
+ self.collection_homepage = primary.resourceHomeUrl
653
+ self.url = urls.get(id(primary))
654
+ active = [
655
+ r for r in namespace.resources if not r.deprecated and id(r) in urls
656
+ ]
657
+ active.sort(key=lambda r: r is not primary)
658
+ self.providers = [
659
+ Provider(name=r.name, url=urls[id(r)], official=bool(r.official))
660
+ for r in active
661
+ ]
662
+
578
663
  # query OLS information
579
664
  self.query_ols()
580
665
 
@@ -591,8 +676,15 @@ class RDFAnnotationData(RDFAnnotation):
591
676
  "collection": self.collection,
592
677
  "term": self.term,
593
678
  "label": self.label,
679
+ "ontology": self.ontology,
680
+ "iri": self.iri,
681
+ "ols_url": self.ols_url,
594
682
  "description": self.description,
595
683
  "url": self.url,
684
+ "providers": [asdict(p) for p in self.providers],
685
+ "collection_name": self.collection_name,
686
+ "collection_homepage": self.collection_homepage,
687
+ "pattern_match": self.pattern_match,
596
688
  "synonyms": self.synonyms,
597
689
  "xrefs": self.xrefs,
598
690
  "errors": self.errors,
@@ -618,6 +710,9 @@ class RDFAnnotationData(RDFAnnotation):
618
710
  if self.description is None:
619
711
  self.description = info["description"]
620
712
 
713
+ self.ontology = info.get("ontology")
714
+ self.iri = info.get("iri")
715
+ self.ols_url = info.get("ols_url")
621
716
  self.synonyms = info["synonyms"]
622
717
  self.xrefs = info["xrefs"]
623
718
  self.warnings.extend(info["warnings"])
@@ -16,6 +16,7 @@ See <https://www.ebi.ac.uk/chebi/>.
16
16
  import contextlib
17
17
  import logging
18
18
  import re
19
+ import tempfile
19
20
  from pathlib import Path
20
21
  from typing import Any
21
22
 
@@ -23,12 +24,18 @@ import pymetadata
23
24
  from pymetadata.cache import (
24
25
  CACHE_DURATION_ONTOLOGY,
25
26
  DataclassJSONEncoder,
27
+ cache_age,
26
28
  cache_file,
27
29
  read_json_cache,
28
30
  read_json_cache_fallback,
29
31
  write_json_cache,
30
32
  )
31
- from pymetadata.webservices.webservice import WebserviceError, get_json
33
+ from pymetadata.webservices.webservice import (
34
+ WebserviceError,
35
+ WebserviceNotFoundError,
36
+ get_bytes,
37
+ get_json,
38
+ )
32
39
 
33
40
  logger = logging.getLogger(__name__)
34
41
 
@@ -38,6 +45,25 @@ CHEBI_PATTERN = re.compile(r"^(?:CHEBI:)?(\d+)$", flags=re.IGNORECASE)
38
45
  #: endpoint of the ChEBI compounds, queried with the `chebi_ids` parameter
39
46
  CHEBI_URL = "https://www.ebi.ac.uk/chebi/backend/api/public/compounds/"
40
47
 
48
+ #: endpoint of the structure (svg image) of a compound, formatted with its number
49
+ CHEBI_STRUCTURE_URL = (
50
+ "https://www.ebi.ac.uk/chebi/backend/api/public/compound/{}/structure/"
51
+ )
52
+
53
+
54
+ def _write_bytes_atomic(path: Path, content: bytes) -> None:
55
+ """Write a file atomically, so a reader never sees a partial file."""
56
+ path.parent.mkdir(parents=True, exist_ok=True)
57
+ temporary_path: Path | None = None
58
+ try:
59
+ with tempfile.NamedTemporaryFile(dir=path.parent, delete=False) as fp:
60
+ temporary_path = Path(fp.name)
61
+ fp.write(content)
62
+ temporary_path.replace(path)
63
+ finally:
64
+ if temporary_path is not None:
65
+ temporary_path.unlink(missing_ok=True)
66
+
41
67
 
42
68
  class ChebiQuery:
43
69
  """Queries against the ChEBI web service.
@@ -95,8 +121,8 @@ class ChebiQuery:
95
121
  chebi_path = cache_file(Path(cache_path) / "chebi", chebi)
96
122
  data: dict[str, Any] = {}
97
123
  if cache:
98
- with contextlib.suppress(OSError):
99
- # cache does not exist or is outdated
124
+ with contextlib.suppress(OSError, ValueError):
125
+ # cache does not exist, is outdated or corrupt
100
126
  data = read_json_cache(
101
127
  cache_path=chebi_path, max_age=CACHE_DURATION_ONTOLOGY
102
128
  )
@@ -144,6 +170,55 @@ class ChebiQuery:
144
170
 
145
171
  return data
146
172
 
173
+ @staticmethod
174
+ def structure(
175
+ chebi: str, cache: bool | None = None, cache_path: Path | None = None
176
+ ) -> bytes | None:
177
+ """Get the structure of a ChEBI compound as an svg image.
178
+
179
+ Args:
180
+ chebi: ChEBI term, e.g., `CHEBI:33699`
181
+ cache: cache the image, defaults to `pymetadata.CACHE_USE`
182
+ cache_path: directory for cached responses, defaults to
183
+ `pymetadata.CACHE_PATH`
184
+
185
+ Returns:
186
+ The svg image, None if the id is invalid, the compound has no
187
+ structure, or ChEBI cannot be reached and nothing is cached.
188
+ """
189
+ match = CHEBI_PATTERN.match(chebi.strip()) if chebi else None
190
+ if not match:
191
+ logger.error("Invalid ChEBI id: '%s'", chebi)
192
+ return None
193
+ number = match.group(1)
194
+ if cache is None:
195
+ cache = pymetadata.CACHE_USE
196
+ if cache_path is None:
197
+ cache_path = pymetadata.CACHE_PATH
198
+ path = Path(cache_path) / "chebi" / f"CHEBI%3A{number}.svg"
199
+ age = cache_age(path) if cache else None
200
+ if age is not None and age <= CACHE_DURATION_ONTOLOGY:
201
+ return path.read_bytes()
202
+ try:
203
+ svg = get_bytes(CHEBI_STRUCTURE_URL.format(number))
204
+ except WebserviceNotFoundError:
205
+ return None
206
+ except WebserviceError as err:
207
+ if age is not None:
208
+ logger.warning(
209
+ "Using outdated structure of 'CHEBI:%s': %s", number, err
210
+ )
211
+ return path.read_bytes()
212
+ logger.error(
213
+ "ChEBI structure could not be retrieved for 'CHEBI:%s': %s",
214
+ number,
215
+ err,
216
+ )
217
+ return None
218
+ if cache:
219
+ _write_bytes_atomic(path, svg)
220
+ return svg
221
+
147
222
 
148
223
  if __name__ == "__main__":
149
224
  from pymetadata.console import console
@@ -35,7 +35,11 @@ from pymetadata.cache import (
35
35
  write_json_cache,
36
36
  )
37
37
  from pymetadata.webservices.registry import get_registry
38
- from pymetadata.webservices.webservice import WebserviceError, get_json
38
+ from pymetadata.webservices.webservice import (
39
+ WebserviceError,
40
+ WebserviceNotFoundError,
41
+ get_json,
42
+ )
39
43
 
40
44
  logger = logging.getLogger(__name__)
41
45
 
@@ -96,6 +100,24 @@ ONTOLOGIES = [
96
100
  ]
97
101
 
98
102
 
103
+ OLS_TERM_PAGE = "https://www.ebi.ac.uk/ols4/ontologies/{}/classes?iri={}"
104
+
105
+
106
+ def ols_term_url(ontology: str | None, iri: str | None) -> str | None:
107
+ """Get the page of a term in the Ontology Lookup Service.
108
+
109
+ Args:
110
+ ontology: ontology id of OLS, e.g., `go`
111
+ iri: IRI of the term
112
+
113
+ Returns:
114
+ The url of the page, or None without an ontology or an IRI.
115
+ """
116
+ if not ontology or not iri:
117
+ return None
118
+ return OLS_TERM_PAGE.format(ontology, urllib.parse.quote(iri, safe=""))
119
+
120
+
99
121
  class OLSQuery:
100
122
  """Queries against the Ontology Lookup Service.
101
123
 
@@ -174,7 +196,8 @@ class OLSQuery:
174
196
 
175
197
  Returns:
176
198
  The OLS response, with `errors` and `warnings` describing problems
177
- with the query.
199
+ with the query. A term OLS does not know is a warning, an ontology
200
+ which is not on OLS returns no information and no message.
178
201
  """
179
202
  if not ontology:
180
203
  return {"errors": [], "warnings": ["No collection."]}
@@ -191,10 +214,7 @@ class OLSQuery:
191
214
  if not namespace or not any(
192
215
  resource.providerCode == "ols" for resource in namespace.resources or []
193
216
  ):
194
- return {
195
- "errors": [],
196
- "warnings": [f"'{ontology}' is not on OLS."],
197
- }
217
+ return {"errors": [], "warnings": []}
198
218
 
199
219
  iri = self.get_iri(ontology=ontology, term=term)
200
220
 
@@ -216,6 +236,8 @@ class OLSQuery:
216
236
  logger.info("Query: %s", url)
217
237
  try:
218
238
  data = get_json(url)
239
+ except WebserviceNotFoundError:
240
+ return {"errors": [], "warnings": [f"Term '{term}' is not on OLS."]}
219
241
  except WebserviceError as err:
220
242
  # prefer outdated information over none, e.g., when offline
221
243
  if self.cache:
@@ -246,7 +268,8 @@ class OLSQuery:
246
268
  term: OLS response from `query_ols`
247
269
 
248
270
  Returns:
249
- Dictionary with `label`, `description`, `synonyms` and `xrefs`.
271
+ Dictionary with `label`, `description`, `synonyms`, `xrefs`,
272
+ `ontology`, `iri` and `ols_url`.
250
273
  """
251
274
  data = {
252
275
  "errors": term["errors"],
@@ -268,10 +291,15 @@ class OLSQuery:
268
291
  synonyms = term.get("obo_synonym", [])
269
292
  xrefs = term.get("obo_xref", [])
270
293
 
294
+ ontology = term.get("ontology_name")
295
+ iri = term.get("iri")
271
296
  return {
272
297
  **data,
273
298
  "label": label,
274
299
  "description": description,
275
300
  "synonyms": synonyms,
276
301
  "xrefs": xrefs,
302
+ "ontology": ontology,
303
+ "iri": iri,
304
+ "ols_url": ols_term_url(ontology, iri),
277
305
  }
@@ -0,0 +1,140 @@
1
+ """Protein information from UniProt.
2
+
3
+ ```python
4
+ from pymetadata.webservices.uniprot import UniprotQuery
5
+
6
+ info = UniprotQuery.query("P69905")
7
+ ```
8
+
9
+ See <https://www.uniprot.org/>.
10
+ """
11
+
12
+ import contextlib
13
+ import logging
14
+ import re
15
+ from pathlib import Path
16
+ from typing import Any
17
+
18
+ import pymetadata
19
+ from pymetadata.cache import (
20
+ CACHE_DURATION_ONTOLOGY,
21
+ cache_file,
22
+ read_json_cache,
23
+ read_json_cache_fallback,
24
+ write_json_cache,
25
+ )
26
+ from pymetadata.webservices.webservice import (
27
+ WebserviceError,
28
+ WebserviceNotFoundError,
29
+ get_json,
30
+ )
31
+
32
+ logger = logging.getLogger(__name__)
33
+
34
+ UNIPROT_URL = "https://rest.uniprot.org/uniprotkb/{}.json"
35
+
36
+ # the accession format of UniProt, with an optional isoform
37
+ UNIPROT_PATTERN = re.compile(
38
+ r"^(?:[OPQ][0-9][A-Z0-9]{3}[0-9]|[A-NR-Z][0-9](?:[A-Z][A-Z0-9]{2}[0-9]){1,2})(?:-\d+)?$"
39
+ )
40
+
41
+
42
+ def _entry_info(entry: dict[str, Any]) -> dict[str, Any]:
43
+ """Reduce an entry of UniProt to the information shown for an annotation."""
44
+ description = entry.get("proteinDescription") or {}
45
+ names = description.get("recommendedName") or next(
46
+ iter(description.get("submissionNames") or []), {}
47
+ )
48
+ function = next(
49
+ (
50
+ comment["texts"][0].get("value")
51
+ for comment in entry.get("comments") or []
52
+ if comment.get("commentType") == "FUNCTION" and comment.get("texts")
53
+ ),
54
+ None,
55
+ )
56
+ return {
57
+ "accession": entry.get("primaryAccession"),
58
+ "entry": entry.get("uniProtkbId"),
59
+ "name": (names.get("fullName") or {}).get("value"),
60
+ "organism": (entry.get("organism") or {}).get("scientificName"),
61
+ "genes": [
62
+ gene["geneName"]["value"]
63
+ for gene in entry.get("genes") or []
64
+ if "geneName" in gene
65
+ ],
66
+ "length": (entry.get("sequence") or {}).get("length"),
67
+ "function": function,
68
+ }
69
+
70
+
71
+ class UniprotQuery:
72
+ """Queries against the UniProt web service.
73
+
74
+ Responses are cached on disk for `CACHE_DURATION_ONTOLOGY` hours, see
75
+ `pymetadata.CACHE_USE`. If UniProt cannot be reached, cached content is used
76
+ however old it is.
77
+ """
78
+
79
+ @staticmethod
80
+ def query(
81
+ accession: str, cache: bool | None = None, cache_path: Path | None = None
82
+ ) -> dict[str, Any]:
83
+ """Query the information of a protein.
84
+
85
+ Args:
86
+ accession: accession of UniProt, e.g., `P69905`
87
+ cache: cache the response, defaults to `pymetadata.CACHE_USE`
88
+ cache_path: directory for cached responses, defaults to
89
+ `pymetadata.CACHE_PATH`
90
+
91
+ Returns:
92
+ The protein information, empty if the accession is invalid, unknown
93
+ or inactive, or if UniProt cannot be reached and nothing is cached.
94
+ """
95
+ accession = accession.strip().upper() if accession else ""
96
+ if not UNIPROT_PATTERN.match(accession):
97
+ logger.error("Invalid UniProt accession: '%s'", accession)
98
+ return {}
99
+ if cache is None:
100
+ cache = pymetadata.CACHE_USE
101
+ if cache_path is None:
102
+ cache_path = pymetadata.CACHE_PATH
103
+
104
+ path = cache_file(Path(cache_path) / "uniprot", accession)
105
+ if cache:
106
+ with contextlib.suppress(OSError, ValueError):
107
+ # cache does not exist, is outdated or corrupt
108
+ data = read_json_cache(cache_path=path, max_age=CACHE_DURATION_ONTOLOGY)
109
+ if data:
110
+ return data
111
+
112
+ try:
113
+ entry = get_json(UNIPROT_URL.format(accession))
114
+ except WebserviceNotFoundError:
115
+ logger.error("UniProt accession is unknown: '%s'", accession)
116
+ return {}
117
+ except WebserviceError as err:
118
+ # prefer outdated information over none, e.g., when offline
119
+ if cache:
120
+ fallback = read_json_cache_fallback(path, reason=str(err))
121
+ if fallback is not None:
122
+ return fallback
123
+ logger.error(
124
+ "UniProt information could not be retrieved for '%s': %s",
125
+ accession,
126
+ err,
127
+ )
128
+ return {}
129
+
130
+ if not isinstance(entry, dict):
131
+ logger.error("Unexpected response of UniProt for '%s'", accession)
132
+ return {}
133
+ if entry.get("entryType") == "Inactive":
134
+ logger.error("UniProt entry is inactive: '%s'", accession)
135
+ return {}
136
+
137
+ data = _entry_info(entry)
138
+ if cache:
139
+ write_json_cache(data=data, cache_path=path)
140
+ return data