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.
- {pymetadata-0.7.0 → pymetadata-0.8.0}/.bumpversion.toml +1 -1
- {pymetadata-0.7.0 → pymetadata-0.8.0}/CITATION.cff +1 -1
- {pymetadata-0.7.0 → pymetadata-0.8.0}/CLAUDE.md +9 -4
- {pymetadata-0.7.0 → pymetadata-0.8.0}/PKG-INFO +1 -1
- {pymetadata-0.7.0 → pymetadata-0.8.0}/docs/annotations.md +8 -2
- {pymetadata-0.7.0 → pymetadata-0.8.0}/docs/api/index.md +2 -1
- pymetadata-0.8.0/docs/api/webservices.uniprot.md +3 -0
- pymetadata-0.8.0/release-notes/0.8.0.md +19 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/src/pymetadata/__init__.py +1 -1
- {pymetadata-0.7.0 → pymetadata-0.8.0}/src/pymetadata/core/annotation.py +100 -5
- {pymetadata-0.7.0 → pymetadata-0.8.0}/src/pymetadata/webservices/chebi.py +78 -3
- {pymetadata-0.7.0 → pymetadata-0.8.0}/src/pymetadata/webservices/ols.py +35 -7
- pymetadata-0.8.0/src/pymetadata/webservices/uniprot.py +140 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/src/pymetadata/webservices/webservice.py +41 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/tests/core/test_annotation.py +202 -6
- {pymetadata-0.7.0 → pymetadata-0.8.0}/tests/test_annotation_resolution.py +1 -1
- {pymetadata-0.7.0 → pymetadata-0.8.0}/tests/test_chebi.py +70 -0
- pymetadata-0.8.0/tests/test_ols.py +148 -0
- pymetadata-0.8.0/tests/test_uniprot.py +129 -0
- pymetadata-0.8.0/tests/test_webservice.py +95 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/zensical.toml +1 -0
- pymetadata-0.7.0/tests/test_ols.py +0 -71
- pymetadata-0.7.0/tests/test_webservice.py +0 -40
- {pymetadata-0.7.0 → pymetadata-0.8.0}/.github/CODEOWNERS +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/.github/dependabot.yml +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/.github/pull_request_template.md +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/.github/rulesets/apply.sh +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/.github/rulesets/develop.json +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/.github/rulesets/main.json +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/.github/rulesets/tags.json +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/.github/workflows/ci-cd.yml +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/.github/workflows/docs.yml +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/.github/workflows/ruff.yml +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/.github/workflows/ty.yml +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/.gitignore +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/.pre-commit-config.yaml +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/.python-version +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/.ruff.toml +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/.zenodo.json +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/LICENSE +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/README.md +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/docs/README.md +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/docs/api/cache.md +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/docs/api/console.md +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/docs/api/core.annotation.md +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/docs/api/core.creator.md +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/docs/api/core.miriam.md +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/docs/api/core.xref.md +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/docs/api/log.md +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/docs/api/omex.md +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/docs/api/ontologies.term.md +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/docs/api/webservices.chebi.md +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/docs/api/webservices.ols.md +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/docs/api/webservices.registry.md +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/docs/api/webservices.unichem.md +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/docs/api/webservices.webservice.md +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/docs/development.md +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/docs/images/HARMONY2026_slides.png +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/docs/images/favicon/about.txt +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/docs/images/favicon/android-chrome-192x192.png +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/docs/images/favicon/android-chrome-512x512.png +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/docs/images/favicon/apple-touch-icon.png +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/docs/images/favicon/favicon-16x16.png +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/docs/images/favicon/favicon-32x32.png +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/docs/images/favicon/favicon.ico +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/docs/images/favicon/favicon_io.zip +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/docs/images/favicon/pymetadata-100x100-300dpi.png +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/docs/images/favicon/site.webmanifest +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/docs/index.md +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/docs/installation.md +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/docs/omex.md +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/docs/robots.txt +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/examples/annotation.py +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/examples/cache_path.py +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/examples/omex/biomodels_omex/MODEL2402290004.4_pymetadata.omex +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/examples/omex/biomodels_omex/create_omex.py +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/examples/omex/biomodels_omex/files/BioModelsMetadata_MODEL2402290004.csv +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/examples/omex/biomodels_omex/files/EndoAcuteBZ1.m +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/examples/omex/biomodels_omex/files/EndoAcuteBZ2.m +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/examples/omex/biomodels_omex/files/EndoAcuteBZ3.m +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/examples/omex/biomodels_omex/files/EndoChronicBZ.m +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/examples/omex/biomodels_omex/files/EndoChronicRZ1.m +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/examples/omex/biomodels_omex/files/EndoChronicRZ2.m +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/examples/omex/biomodels_omex/files/EndoNZ.m +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/examples/omex/biomodels_omex/files/POM253.mat +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/examples/omex/biomodels_omex/files/PlotAPCaTTaAcuteAndChronic.m +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/examples/omex/biomodels_omex/files/Zhou2024_Updated-model.m +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/examples/omex/biomodels_omex/files/getCurrentsStructureCaMKSKJrelpKinetics.m +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/examples/omex/biomodels_omex/files/getStartingState.m +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/examples/omex/biomodels_omex/files/main.m +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/examples/omex/biomodels_omex/files/manifest.xml +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/examples/omex/biomodels_omex/files/metadata.rdf +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/examples/omex/biomodels_omex/files/modelRunnerCaMKSKJrelpKinetics1ms.m +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/examples/omex/biomodels_omex/files/model_ToRORd_LandCaMKSKJrelpKinetics.m +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/examples/omex/omex.py +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/examples/omex/omex_from_url.py +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/examples/omex/test.omex +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/examples/pbpko.py +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/examples/sbo.py +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/presentations/HARMONY2026/.gitignore +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/presentations/HARMONY2026/README.md +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/presentations/HARMONY2026/_extensions/grantmcdermott/clean/_extension.yml +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/presentations/HARMONY2026/_extensions/grantmcdermott/clean/clean.scss +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/presentations/HARMONY2026/_extensions/grantmcdermott/clean/mathjax-config.js +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/presentations/HARMONY2026/custom.scss +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/presentations/HARMONY2026/images/annotation_example.png +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/presentations/HARMONY2026/images/annotation_resolved.png +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/presentations/HARMONY2026/images/annotation_resolved_small.png +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/presentations/HARMONY2026/images/github_screenshot.png +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/presentations/HARMONY2026/images/headshot.png +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/presentations/HARMONY2026/images/omex_logo.png +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/presentations/HARMONY2026/images/omex_overview.png +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/presentations/HARMONY2026/images/pymetadata.png +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/presentations/HARMONY2026/images/pymetadata_large.png +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/presentations/HARMONY2026/images/qr_livermetabolism.png +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/presentations/HARMONY2026/mathjax-config.js +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/presentations/HARMONY2026/pymetadata.bib +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/presentations/HARMONY2026/pymetadata.qmd +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/presentations/HARMONY2026/styles.css +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/pyproject.toml +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.0.10.md +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.0.11.md +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.0.12.md +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.0.13.md +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.0.14.md +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.0.15.md +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.0.16.md +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.0.17.md +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.0.18.md +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.0.19.md +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.0.2.md +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.0.20.md +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.0.21.md +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.0.22.md +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.0.3.md +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.0.4.md +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.0.5.md +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.0.6.md +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.0.7.md +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.0.8.md +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.0.9.md +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.1.0.md +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.2.1.md +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.2.10.md +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.2.2.md +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.2.3.md +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.2.4.md +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.2.5.md +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.2.6.md +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.2.7.md +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.2.8.md +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.2.9.md +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.3.0.md +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.3.1.md +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.3.10.md +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.3.11.md +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.3.2.md +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.3.3.md +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.3.4.md +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.3.5.md +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.3.6.md +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.3.7.md +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.3.8.md +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.3.9.md +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.4.0.md +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.4.1.md +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.4.2.md +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.4.3.md +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.4.4.md +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.5.0.md +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.5.1.md +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.5.10.md +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.5.11.md +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.5.12.md +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.5.2.md +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.5.3.md +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.5.4.md +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.5.5.md +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.5.6.md +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.5.7.md +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.5.8.md +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.5.9.md +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.6.0.md +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.6.1.md +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.6.2.md +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.6.3.md +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.6.4.md +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.6.5.md +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.6.6.md +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/release-notes/0.7.0.md +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/scripts/llms_txt.py +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/src/pymetadata/_files.py +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/src/pymetadata/cache.py +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/src/pymetadata/console.py +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/src/pymetadata/core/__init__.py +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/src/pymetadata/core/creator.py +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/src/pymetadata/core/miriam.py +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/src/pymetadata/core/synonym.py +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/src/pymetadata/core/xref.py +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/src/pymetadata/log.py +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/src/pymetadata/omex.py +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/src/pymetadata/ontologies/__init__.py +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/src/pymetadata/ontologies/_ontology_builder.py +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/src/pymetadata/ontologies/kisao.py +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/src/pymetadata/ontologies/pbpko.py +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/src/pymetadata/ontologies/sbo.py +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/src/pymetadata/ontologies/term.py +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/src/pymetadata/py.typed +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/src/pymetadata/resources/ontologies/README.md +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/src/pymetadata/webservices/__init__.py +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/src/pymetadata/webservices/registry.py +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/src/pymetadata/webservices/unichem.py +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/tests/core/test_creator.py +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/tests/data/omex/BIOMD0000000001.omex +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/tests/data/omex/CombineArchiveShowCase.omex +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/tests/data/omex/CombineArchiveShowCase_manifest.xml +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/tests/data/omex/CompModels.omex +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/tests/data/omex/CompModels_manifest.xml +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/tests/data/omex/iCGB21FR.omex +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/tests/test_cache.py +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/tests/test_log.py +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/tests/test_offline.py +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/tests/test_omex.py +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/tests/test_ontology.py +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/tests/test_ontology_builder.py +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/tests/test_registry.py +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/tests/test_sbo_kisao.py +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/tests/test_unichem.py +0 -0
- {pymetadata-0.7.0 → pymetadata-0.8.0}/tox.ini +0 -0
|
@@ -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.
|
|
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
|
|
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.
|
|
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
|
|
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,19 @@
|
|
|
1
|
+
# Release notes for pymetadata 0.8.0
|
|
2
|
+

|
|
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
|
|
@@ -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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|