linkml-reference-validator 0.2.1rc1__tar.gz → 0.2.1rc2__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.
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/PKG-INFO +30 -4
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/README.md +29 -3
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/docs/how-to/fetch-full-text-and-pdfs.md +49 -3
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/src/linkml_reference_validator/etl/fulltext/__init__.py +2 -0
- linkml_reference_validator-0.2.1rc2/src/linkml_reference_validator/etl/fulltext/epmc_preprint.py +142 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/src/linkml_reference_validator/etl/identifiers.py +5 -1
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/src/linkml_reference_validator/etl/reference_fetcher.py +8 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/src/linkml_reference_validator/etl/sources/__init__.py +2 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/src/linkml_reference_validator/etl/sources/doi.py +34 -0
- linkml_reference_validator-0.2.1rc2/src/linkml_reference_validator/etl/sources/ppr.py +159 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/src/linkml_reference_validator/models.py +28 -3
- linkml_reference_validator-0.2.1rc2/tests/test_doi_preprint.py +103 -0
- linkml_reference_validator-0.2.1rc2/tests/test_epmc_preprint_provider.py +196 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/tests/test_identifiers.py +25 -0
- linkml_reference_validator-0.2.1rc2/tests/test_ppr_source.py +125 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/tests/test_reference_fetcher.py +37 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/tests/test_validation_config.py +1 -1
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/.claude/.mcp.json +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/.claude/settings.json +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/.copier-answers.yml +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/.devcontainer/devcontainer.json +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/.devcontainer/post_create.sh +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/.editorconfig +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/.github/actions/claude-code-action/action.yml +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/.github/actions/claude-issue-summarize-action/action.yml +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/.github/actions/claude-issue-triage-action/action.yml +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/.github/ai-controllers.json +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/.github/copilot-instructions.md +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/.github/copilot-setup-steps.yml +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/.github/dependabot.yml +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/.github/workflows/claude-code-review.yml +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/.github/workflows/claude-issue-summarize.yml +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/.github/workflows/claude-issue-triage.yml +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/.github/workflows/claude-ontology-review.yml +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/.github/workflows/claude.yml +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/.github/workflows/deploy-docs.yaml +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/.github/workflows/dragon-ai.yml +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/.github/workflows/main.yaml +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/.github/workflows/pypi-publish.yaml +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/.gitignore +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/.goosehints +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/.pre-commit-config.yaml +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/.yamllint.yaml +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/AGENTS.md +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/BUG_GEO_ACCESSION_TO_UID.md +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/BUG_ZENODO_DOI_AND_URL_HANDLING.md +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/CLAUDE.md +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/CLI-ISSUES.md +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/CODE_OF_CONDUCT.md +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/CONTRIBUTING.md +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/FEATURE_REQUEST_REPAIR.md +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/LICENSE +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/REVIEW2.md +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/REVIEW3.md +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/ai.just +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/copilot-instructions.md +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/docs/about.md +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/docs/concepts/content-types.md +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/docs/concepts/editorial-conventions.md +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/docs/concepts/how-it-works.md +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/docs/elements/.gitkeep +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/docs/how-to/add-reference-source.md +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/docs/how-to/repair-validation-errors.md +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/docs/how-to/skip-unsupported-references.md +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/docs/how-to/use-local-files-and-urls.md +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/docs/how-to/validate-clinical-trials.md +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/docs/how-to/validate-dois.md +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/docs/how-to/validate-entrez.md +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/docs/how-to/validate-obo-files.md +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/docs/how-to/validate-titles.md +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/docs/how-to/validate-urls.md +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/docs/index.md +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/docs/notebooks/01_getting_started.ipynb +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/docs/notebooks/02_advanced_usage.ipynb +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/docs/notebooks/03_python_api.ipynb +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/docs/notebooks/04_obo_validation.ipynb +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/docs/notebooks/05_geo_validation.ipynb +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/docs/quickstart.md +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/docs/reference/cli.md +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/docs/setup-guide.md +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/docs/superpowers/plans/2026-06-12-modular-pdf-fulltext-fetching.md +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/docs/superpowers/specs/2026-06-12-modular-pdf-fulltext-fetching-design.md +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/docs/templates-linkml/README.md +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/docs/todo.md +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/docs/troubleshooting.md +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/docs/tutorials/complete-workflow.md +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/examples/README.md +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/justfile +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/mkdocs.yml +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/mypy.ini +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/notebooks/01_getting_started.ipynb +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/notebooks/02_advanced_usage.ipynb +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/notebooks/03_python_api.ipynb +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/notebooks/README.md +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/project/README.md +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/project.justfile +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/pyproject.toml +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/pytest.ini +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/src/linkml_reference_validator/__init__.py +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/src/linkml_reference_validator/_version.py +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/src/linkml_reference_validator/cli/__init__.py +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/src/linkml_reference_validator/cli/cache.py +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/src/linkml_reference_validator/cli/lookup.py +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/src/linkml_reference_validator/cli/repair.py +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/src/linkml_reference_validator/cli/shared.py +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/src/linkml_reference_validator/cli/validate.py +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/src/linkml_reference_validator/etl/__init__.py +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/src/linkml_reference_validator/etl/acquire.py +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/src/linkml_reference_validator/etl/extract/__init__.py +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/src/linkml_reference_validator/etl/extract/base.py +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/src/linkml_reference_validator/etl/extract/html.py +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/src/linkml_reference_validator/etl/extract/pdf.py +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/src/linkml_reference_validator/etl/extract/xml.py +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/src/linkml_reference_validator/etl/fulltext/base.py +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/src/linkml_reference_validator/etl/fulltext/json_api.py +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/src/linkml_reference_validator/etl/fulltext/loader.py +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/src/linkml_reference_validator/etl/fulltext/openalex.py +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/src/linkml_reference_validator/etl/fulltext/pmc.py +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/src/linkml_reference_validator/etl/fulltext/unpaywall.py +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/src/linkml_reference_validator/etl/sources/base.py +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/src/linkml_reference_validator/etl/sources/clinicaltrials.py +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/src/linkml_reference_validator/etl/sources/entrez.py +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/src/linkml_reference_validator/etl/sources/file.py +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/src/linkml_reference_validator/etl/sources/json_api.py +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/src/linkml_reference_validator/etl/sources/loader.py +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/src/linkml_reference_validator/etl/sources/pmid.py +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/src/linkml_reference_validator/etl/sources/url.py +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/src/linkml_reference_validator/etl/sources/utils.py +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/src/linkml_reference_validator/etl/text_extractor.py +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/src/linkml_reference_validator/field_detection.py +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/src/linkml_reference_validator/plugins/REVIEW.md +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/src/linkml_reference_validator/plugins/__init__.py +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/src/linkml_reference_validator/plugins/reference_validation_plugin.py +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/src/linkml_reference_validator/validation/__init__.py +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/src/linkml_reference_validator/validation/fuzzy_text_utils.py +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/src/linkml_reference_validator/validation/repairer.py +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/src/linkml_reference_validator/validation/supporting_text_validator.py +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/tests/__init__.py +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/tests/conftest.py +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/tests/data/test_data_deep_nesting_invalid.yaml +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/tests/data/test_data_deep_nesting_valid.yaml +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/tests/data/test_data_invalid.yaml +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/tests/data/test_data_valid.yaml +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/tests/data/test_schema.yaml +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/tests/data/test_schema_deep_nesting.yaml +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/tests/fixtures/CLINICALTRIALS_NCT00000001.md +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/tests/fixtures/GEO_GSE12345.md +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/tests/fixtures/PMC_TEST001.md +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/tests/fixtures/PMC_TEST001.txt +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/tests/fixtures/PMC_TEST002.md +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/tests/fixtures/PMC_TEST002.txt +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/tests/fixtures/PMID_TEST001.md +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/tests/fixtures/PMID_TEST001.txt +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/tests/fixtures/PMID_TEST002.md +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/tests/fixtures/PMID_TEST002.txt +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/tests/fixtures/PMID_TEST003.md +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/tests/fixtures/PMID_TEST003.txt +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/tests/fixtures/sample.obo +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/tests/test_acquire.py +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/tests/test_cli.py +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/tests/test_cli_lookup.py +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/tests/test_cli_repair.py +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/tests/test_clinicaltrials_integration.py +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/tests/test_deep_nesting.py +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/tests/test_e2e_integration.py +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/tests/test_extractors.py +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/tests/test_field_detection.py +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/tests/test_fulltext_loader.py +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/tests/test_fulltext_providers.py +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/tests/test_fuzzy_suggestions.py +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/tests/test_geo_integration.py +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/tests/test_json_api_source.py +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/tests/test_models.py +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/tests/test_optional_linkml_dependency.py +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/tests/test_plugin_integration.py +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/tests/test_pmc_fulltext.py +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/tests/test_real_validation.py +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/tests/test_repair.py +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/tests/test_source_utils.py +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/tests/test_sources.py +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/tests/test_supporting_text_validator.py +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/tests/test_text_extractor.py +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/tests/test_title_validation.py +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/tests/test_unknown_slot_traversal.py +0 -0
- {linkml_reference_validator-0.2.1rc1 → linkml_reference_validator-0.2.1rc2}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: linkml_reference_validator
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.1rc2
|
|
4
4
|
Summary: Validation of supporting text from references and publications
|
|
5
5
|
Author-email: Chris Mungall <cjmungall@lbl.gov>
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -395,10 +395,36 @@ reference:
|
|
|
395
395
|
- `PMID:12345678`
|
|
396
396
|
- `12345678` (assumes PMID)
|
|
397
397
|
|
|
398
|
-
|
|
398
|
+
#### Preprints
|
|
399
399
|
|
|
400
|
-
|
|
401
|
-
|
|
400
|
+
Preprints are supported as first-class references:
|
|
401
|
+
|
|
402
|
+
```yaml
|
|
403
|
+
reference:
|
|
404
|
+
id: DOI:10.1101/2024.01.01.573333 # by DOI
|
|
405
|
+
supporting_text: "the kinase phosphorylates the substrate in vitro"
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
```yaml
|
|
409
|
+
reference:
|
|
410
|
+
id: PPR:PPR123456 # by Europe PMC preprint id
|
|
411
|
+
supporting_text: "the kinase phosphorylates the substrate in vitro"
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
**Fetches:**
|
|
415
|
+
- Abstract and metadata (title, authors, server, year, DOI)
|
|
416
|
+
- Full-text body via the Europe PMC `fulltextRepo` PDF route (`epmc_preprint`
|
|
417
|
+
provider), for the Europe-PMC-ingested subset
|
|
418
|
+
- `is_preprint` / `peer_review_status` flags, so downstream KBs can apply
|
|
419
|
+
"not sole support" policies
|
|
420
|
+
|
|
421
|
+
**ID Formats:**
|
|
422
|
+
- `DOI:10.1101/...` (Crossref `posted-content` is detected as a preprint)
|
|
423
|
+
- `PPR:PPR123456` (Europe PMC `SRC:PPR` id)
|
|
424
|
+
- NIH Preprint Pilot preprints carry real PMIDs and resolve via the `PMID:` path
|
|
425
|
+
|
|
426
|
+
See the [full-text guide](docs/how-to/fetch-full-text-and-pdfs.md#preprints) for
|
|
427
|
+
details and coverage caveats.
|
|
402
428
|
|
|
403
429
|
---
|
|
404
430
|
|
|
@@ -374,10 +374,36 @@ reference:
|
|
|
374
374
|
- `PMID:12345678`
|
|
375
375
|
- `12345678` (assumes PMID)
|
|
376
376
|
|
|
377
|
-
|
|
377
|
+
#### Preprints
|
|
378
378
|
|
|
379
|
-
|
|
380
|
-
|
|
379
|
+
Preprints are supported as first-class references:
|
|
380
|
+
|
|
381
|
+
```yaml
|
|
382
|
+
reference:
|
|
383
|
+
id: DOI:10.1101/2024.01.01.573333 # by DOI
|
|
384
|
+
supporting_text: "the kinase phosphorylates the substrate in vitro"
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
```yaml
|
|
388
|
+
reference:
|
|
389
|
+
id: PPR:PPR123456 # by Europe PMC preprint id
|
|
390
|
+
supporting_text: "the kinase phosphorylates the substrate in vitro"
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
**Fetches:**
|
|
394
|
+
- Abstract and metadata (title, authors, server, year, DOI)
|
|
395
|
+
- Full-text body via the Europe PMC `fulltextRepo` PDF route (`epmc_preprint`
|
|
396
|
+
provider), for the Europe-PMC-ingested subset
|
|
397
|
+
- `is_preprint` / `peer_review_status` flags, so downstream KBs can apply
|
|
398
|
+
"not sole support" policies
|
|
399
|
+
|
|
400
|
+
**ID Formats:**
|
|
401
|
+
- `DOI:10.1101/...` (Crossref `posted-content` is detected as a preprint)
|
|
402
|
+
- `PPR:PPR123456` (Europe PMC `SRC:PPR` id)
|
|
403
|
+
- NIH Preprint Pilot preprints carry real PMIDs and resolve via the `PMID:` path
|
|
404
|
+
|
|
405
|
+
See the [full-text guide](docs/how-to/fetch-full-text-and-pdfs.md#preprints) for
|
|
406
|
+
details and coverage caveats.
|
|
381
407
|
|
|
382
408
|
---
|
|
383
409
|
|
|
@@ -21,11 +21,13 @@ The validator tries providers in order until one returns usable full text.
|
|
|
21
21
|
The default order is:
|
|
22
22
|
|
|
23
23
|
```
|
|
24
|
-
pmc → unpaywall → openalex
|
|
24
|
+
pmc → epmc_preprint → unpaywall → openalex
|
|
25
25
|
```
|
|
26
26
|
|
|
27
27
|
- **`pmc`** — PubMed Central open-access subset (XML/HTML full text for many
|
|
28
28
|
biomedical articles).
|
|
29
|
+
- **`epmc_preprint`** — Europe PMC preprint body text via the `fulltextRepo`
|
|
30
|
+
PDF route (see [Preprints](#preprints) below).
|
|
29
31
|
- **`unpaywall`** — Unpaywall open-access lookup by DOI (often a publisher or
|
|
30
32
|
repository PDF).
|
|
31
33
|
- **`openalex`** — OpenAlex open-access location (PDF/HTML).
|
|
@@ -43,7 +45,7 @@ your config YAML or on the `ReferenceValidationConfig` object):
|
|
|
43
45
|
| Key | Default | Description |
|
|
44
46
|
|-----|---------|-------------|
|
|
45
47
|
| `fetch_full_text` | `true` | Attempt to obtain full text via the provider chain when a metadata source does not already return full text. |
|
|
46
|
-
| `full_text_providers` | `[pmc, unpaywall, openalex]` | Ordered list of provider names to try until one yields usable full text. |
|
|
48
|
+
| `full_text_providers` | `[pmc, epmc_preprint, unpaywall, openalex]` | Ordered list of provider names to try until one yields usable full text. |
|
|
47
49
|
| `pdf_backend` | `pypdf` | Name of the PDF text-extraction backend. |
|
|
48
50
|
| `download_pdfs` | `true` | If true, persist downloaded PDFs to the files cache directory. |
|
|
49
51
|
| `full_text_providers_file` | `null` | Optional path to a YAML file defining custom full-text providers. |
|
|
@@ -62,12 +64,56 @@ email: you@example.org
|
|
|
62
64
|
fetch_full_text: true
|
|
63
65
|
full_text_providers:
|
|
64
66
|
- pmc
|
|
67
|
+
- epmc_preprint
|
|
65
68
|
- unpaywall
|
|
66
69
|
- openalex
|
|
67
70
|
pdf_backend: pypdf
|
|
68
71
|
download_pdfs: true
|
|
69
72
|
```
|
|
70
73
|
|
|
74
|
+
## Preprints
|
|
75
|
+
|
|
76
|
+
Preprints are first-class references. They are increasingly cited for early
|
|
77
|
+
mechanistic findings, but they sit in two distinct universes that need
|
|
78
|
+
different handling:
|
|
79
|
+
|
|
80
|
+
- **NIH Preprint Pilot** — a subset of preprints carry real PMIDs, so they
|
|
81
|
+
resolve through the normal `PMID:` path with no special handling.
|
|
82
|
+
- **Europe PMC `SRC:PPR`** — the much larger preprint firehose is DOI-only
|
|
83
|
+
(no PMIDs). These resolve by DOI through Crossref, or directly by their
|
|
84
|
+
Europe PMC preprint id using the `PPR:` prefix (e.g. `PPR:PPR123456`).
|
|
85
|
+
|
|
86
|
+
### Full-text retrieval
|
|
87
|
+
|
|
88
|
+
Preprint bodies are **not** served by the PMC `fullTextXML` endpoint — even the
|
|
89
|
+
~7% with a PMCID expose only abstract/title/supplementary in PMC, and direct
|
|
90
|
+
`biorxiv.org/...full.pdf` links sit behind a bot challenge. The route that
|
|
91
|
+
works for automated clients is the Europe PMC `fulltextRepo` PDF, used by the
|
|
92
|
+
`epmc_preprint` provider: it resolves a preprint by DOI (or `SRC:PPR` id),
|
|
93
|
+
finds the open `fulltextRepo` PDF URL, and downloads it. The PDF's `%PDF-`
|
|
94
|
+
magic bytes are validated before extraction, so a stale-filename error blob is
|
|
95
|
+
rejected rather than fed to the PDF parser, and the chain falls through
|
|
96
|
+
gracefully.
|
|
97
|
+
|
|
98
|
+
Coverage note: the `epmc_preprint` route covers the Europe-PMC-ingested
|
|
99
|
+
preprint subset. The newest openRxiv DOIs (prefix `10.64898`) are not yet in
|
|
100
|
+
Europe PMC, so very recent preprints may resolve to metadata only until they
|
|
101
|
+
are ingested.
|
|
102
|
+
|
|
103
|
+
### Preprint / peer-review status
|
|
104
|
+
|
|
105
|
+
When a reference is detected as a preprint — via the Crossref `posted-content`
|
|
106
|
+
type or a Europe PMC `SRC:PPR` record — the cached reference records it:
|
|
107
|
+
|
|
108
|
+
| Field | Description |
|
|
109
|
+
|-------|-------------|
|
|
110
|
+
| `is_preprint` | `true` for a preprint, `false` when the source authoritatively classifies it as not a preprint (e.g. a Crossref `journal-article`), and unset when the type is unknown. |
|
|
111
|
+
| `peer_review_status` | `preprint` for preprints; left unset otherwise. |
|
|
112
|
+
|
|
113
|
+
Downstream knowledge bases can use these fields to apply policies such as "a
|
|
114
|
+
preprint may not be the sole support for a claim". A non-preprint is not
|
|
115
|
+
positively asserted as `peer_reviewed`; `peer_review_status` stays unset.
|
|
116
|
+
|
|
71
117
|
## CLI flag
|
|
72
118
|
|
|
73
119
|
Every `validate` subcommand accepts a `--full-text/--no-full-text` flag that
|
|
@@ -122,7 +168,7 @@ the text came from. These provenance fields are written alongside the content:
|
|
|
122
168
|
|
|
123
169
|
| Field | Description |
|
|
124
170
|
|-------|-------------|
|
|
125
|
-
| `full_text_provider` | Name of the provider that supplied the full text (e.g. `pmc`, `unpaywall`, or a custom name). |
|
|
171
|
+
| `full_text_provider` | Name of the provider that supplied the full text (e.g. `pmc`, `epmc_preprint`, `unpaywall`, or a custom name). |
|
|
126
172
|
| `oa_status` | Open-access status reported by the provider (e.g. `gold`, `green`, `bronze`). |
|
|
127
173
|
| `license` | License of the open-access copy, when known. |
|
|
128
174
|
| `local_pdf_path` | Path (relative to the cache directory) to the downloaded PDF, when one was persisted. |
|
|
@@ -7,6 +7,7 @@ from linkml_reference_validator.etl.fulltext.base import (
|
|
|
7
7
|
|
|
8
8
|
# Import providers to register them
|
|
9
9
|
from linkml_reference_validator.etl.fulltext.pmc import PMCFullTextProvider
|
|
10
|
+
from linkml_reference_validator.etl.fulltext.epmc_preprint import EuropePMCPreprintProvider
|
|
10
11
|
from linkml_reference_validator.etl.fulltext.unpaywall import UnpaywallProvider
|
|
11
12
|
from linkml_reference_validator.etl.fulltext.openalex import OpenAlexProvider
|
|
12
13
|
|
|
@@ -14,6 +15,7 @@ __all__ = [
|
|
|
14
15
|
"FullTextProvider",
|
|
15
16
|
"FullTextProviderRegistry",
|
|
16
17
|
"PMCFullTextProvider",
|
|
18
|
+
"EuropePMCPreprintProvider",
|
|
17
19
|
"UnpaywallProvider",
|
|
18
20
|
"OpenAlexProvider",
|
|
19
21
|
]
|
linkml_reference_validator-0.2.1rc2/src/linkml_reference_validator/etl/fulltext/epmc_preprint.py
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"""Europe PMC preprint full-text provider.
|
|
2
|
+
|
|
3
|
+
Preprints (Europe PMC ``SRC:PPR`` records) are not served by the PMC
|
|
4
|
+
``fullTextXML`` endpoint and their bodies are absent from PMC even when a PMCID
|
|
5
|
+
exists. The one route that reliably serves a preprint body is the Europe PMC
|
|
6
|
+
``fulltextRepo`` PDF: every ``SRC:PPR AND HAS_FT:Y`` core record carries a direct
|
|
7
|
+
``/fulltextRepo?pprId=...`` PDF URL in its ``fullTextUrlList``.
|
|
8
|
+
|
|
9
|
+
This provider resolves that URL from a DOI (or a ``SRC:PPR`` id) and returns it as
|
|
10
|
+
a ``FullTextLocation``. The downstream acquire/extract machinery validates the
|
|
11
|
+
``%PDF-`` magic bytes (a minority of records return a stale-filename error blob),
|
|
12
|
+
so an HTML/error response is rejected rather than fed to the PDF parser.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import logging
|
|
16
|
+
import time
|
|
17
|
+
from typing import Optional
|
|
18
|
+
|
|
19
|
+
import requests # type: ignore
|
|
20
|
+
|
|
21
|
+
from linkml_reference_validator.models import (
|
|
22
|
+
FullTextLocation,
|
|
23
|
+
ReferenceIdentifiers,
|
|
24
|
+
ReferenceValidationConfig,
|
|
25
|
+
)
|
|
26
|
+
from linkml_reference_validator.etl.fulltext.base import (
|
|
27
|
+
FullTextProvider,
|
|
28
|
+
FullTextProviderRegistry,
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
logger = logging.getLogger(__name__)
|
|
32
|
+
|
|
33
|
+
_EPMC_SEARCH_URL = "https://www.ebi.ac.uk/europepmc/webservices/rest/search"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@FullTextProviderRegistry.register
|
|
37
|
+
class EuropePMCPreprintProvider(FullTextProvider):
|
|
38
|
+
"""Locate preprint full text via the Europe PMC fulltextRepo PDF route.
|
|
39
|
+
|
|
40
|
+
Examples:
|
|
41
|
+
>>> EuropePMCPreprintProvider.name()
|
|
42
|
+
'epmc_preprint'
|
|
43
|
+
"""
|
|
44
|
+
|
|
45
|
+
@classmethod
|
|
46
|
+
def name(cls) -> str:
|
|
47
|
+
return "epmc_preprint"
|
|
48
|
+
|
|
49
|
+
def locate(
|
|
50
|
+
self, ids: ReferenceIdentifiers, config: ReferenceValidationConfig
|
|
51
|
+
) -> Optional[FullTextLocation]:
|
|
52
|
+
# Skip the Europe PMC round-trip for records the metadata source already
|
|
53
|
+
# confirmed are peer-reviewed (is_preprint False). A None ("unknown", e.g.
|
|
54
|
+
# a PMID or DataCite record) is still attempted.
|
|
55
|
+
if ids.is_preprint is False:
|
|
56
|
+
return None
|
|
57
|
+
|
|
58
|
+
query = self._build_query(ids)
|
|
59
|
+
if query is None:
|
|
60
|
+
return None
|
|
61
|
+
|
|
62
|
+
time.sleep(config.rate_limit_delay)
|
|
63
|
+
params = {
|
|
64
|
+
"query": query,
|
|
65
|
+
"format": "json",
|
|
66
|
+
"resultType": "core",
|
|
67
|
+
"pageSize": "1",
|
|
68
|
+
"email": config.email,
|
|
69
|
+
}
|
|
70
|
+
response = requests.get(_EPMC_SEARCH_URL, params=params, timeout=30)
|
|
71
|
+
if response.status_code != 200:
|
|
72
|
+
logger.debug(f"Europe PMC search returned {response.status_code} for {query}")
|
|
73
|
+
return None
|
|
74
|
+
|
|
75
|
+
result = self._first_ppr_result(response.json())
|
|
76
|
+
if result is None:
|
|
77
|
+
return None
|
|
78
|
+
|
|
79
|
+
pdf_url = self._extract_pdf_url(result)
|
|
80
|
+
if not pdf_url:
|
|
81
|
+
return None
|
|
82
|
+
|
|
83
|
+
return FullTextLocation(
|
|
84
|
+
url=pdf_url,
|
|
85
|
+
format_hint="pdf",
|
|
86
|
+
oa_status="green",
|
|
87
|
+
license=result.get("license"),
|
|
88
|
+
version="preprint",
|
|
89
|
+
provider="epmc_preprint",
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
def _build_query(self, ids: ReferenceIdentifiers) -> Optional[str]:
|
|
93
|
+
"""Build a Europe PMC query restricted to the preprint source.
|
|
94
|
+
|
|
95
|
+
Examples:
|
|
96
|
+
>>> p = EuropePMCPreprintProvider()
|
|
97
|
+
>>> p._build_query(ReferenceIdentifiers(doi="10.1101/x"))
|
|
98
|
+
'DOI:"10.1101/x" AND SRC:PPR'
|
|
99
|
+
>>> p._build_query(ReferenceIdentifiers(pprid="PPR42"))
|
|
100
|
+
'EXT_ID:PPR42 AND SRC:PPR'
|
|
101
|
+
>>> p._build_query(ReferenceIdentifiers(pmid="123")) is None
|
|
102
|
+
True
|
|
103
|
+
"""
|
|
104
|
+
if ids.doi:
|
|
105
|
+
return f'DOI:"{ids.doi}" AND SRC:PPR'
|
|
106
|
+
if ids.pprid:
|
|
107
|
+
return f"EXT_ID:{ids.pprid} AND SRC:PPR"
|
|
108
|
+
return None
|
|
109
|
+
|
|
110
|
+
def _first_ppr_result(self, data: dict) -> Optional[dict]:
|
|
111
|
+
"""Return the first ``SRC:PPR`` result, or None.
|
|
112
|
+
|
|
113
|
+
Restricting to ``source == "PPR"`` guards against a peer-reviewed record
|
|
114
|
+
sharing the DOI namespace being mistaken for a preprint.
|
|
115
|
+
"""
|
|
116
|
+
results = data.get("resultList", {}).get("result", [])
|
|
117
|
+
for result in results:
|
|
118
|
+
if isinstance(result, dict) and result.get("source") == "PPR":
|
|
119
|
+
return result
|
|
120
|
+
return None
|
|
121
|
+
|
|
122
|
+
def _extract_pdf_url(self, result: dict) -> Optional[str]:
|
|
123
|
+
"""Find the fulltextRepo PDF URL for a preprint core record.
|
|
124
|
+
|
|
125
|
+
Returns the ``documentStyle == "pdf"`` entry from ``fullTextUrlList`` (the
|
|
126
|
+
Europe PMC ``fulltextRepo`` PDF). The full URL must be taken verbatim from
|
|
127
|
+
the record: the working endpoint includes a per-record ``fileName`` query
|
|
128
|
+
parameter that cannot be reconstructed from the preprint id alone (a
|
|
129
|
+
``fileName``-less request 500s), so a record without a usable PDF entry
|
|
130
|
+
yields no location rather than a guessed URL.
|
|
131
|
+
"""
|
|
132
|
+
full_text_urls = result.get("fullTextUrlList", {}).get("fullTextUrl", [])
|
|
133
|
+
for entry in full_text_urls:
|
|
134
|
+
if not isinstance(entry, dict):
|
|
135
|
+
continue
|
|
136
|
+
url = entry.get("url")
|
|
137
|
+
if not url:
|
|
138
|
+
continue
|
|
139
|
+
if entry.get("documentStyle") == "pdf" or "fulltextRepo" in url:
|
|
140
|
+
return url
|
|
141
|
+
|
|
142
|
+
return None
|
|
@@ -39,10 +39,12 @@ def build_identifiers(content: ReferenceContent) -> ReferenceIdentifiers:
|
|
|
39
39
|
>>> ids = build_identifiers(ReferenceContent(reference_id="PMID:9", doi="10.1/z"))
|
|
40
40
|
>>> ids.pmid, ids.doi
|
|
41
41
|
('9', '10.1/z')
|
|
42
|
+
>>> build_identifiers(ReferenceContent(reference_id="PPR:PPR42")).pprid
|
|
43
|
+
'PPR42'
|
|
42
44
|
"""
|
|
43
45
|
prefix, identifier = _split_reference_id(content.reference_id)
|
|
44
46
|
|
|
45
|
-
ids = ReferenceIdentifiers(doi=content.doi or None)
|
|
47
|
+
ids = ReferenceIdentifiers(doi=content.doi or None, is_preprint=content.is_preprint)
|
|
46
48
|
|
|
47
49
|
if prefix and identifier:
|
|
48
50
|
upper = prefix.upper()
|
|
@@ -50,6 +52,8 @@ def build_identifiers(content: ReferenceContent) -> ReferenceIdentifiers:
|
|
|
50
52
|
ids.pmid = identifier
|
|
51
53
|
elif upper == "PMCID":
|
|
52
54
|
ids.pmcid = identifier
|
|
55
|
+
elif upper == "PPR":
|
|
56
|
+
ids.pprid = identifier
|
|
53
57
|
elif upper == "DOI" and not ids.doi:
|
|
54
58
|
ids.doi = identifier
|
|
55
59
|
elif prefix.lower() == "url":
|
|
@@ -492,6 +492,12 @@ class ReferenceFetcher:
|
|
|
492
492
|
for keyword in reference.keywords:
|
|
493
493
|
lines.append(f"- {self._quote_yaml_value(keyword)}")
|
|
494
494
|
lines.append(f"content_type: {reference.content_type}")
|
|
495
|
+
if reference.is_preprint is not None:
|
|
496
|
+
lines.append(f"is_preprint: {str(reference.is_preprint).lower()}")
|
|
497
|
+
if reference.peer_review_status:
|
|
498
|
+
lines.append(
|
|
499
|
+
f"peer_review_status: {self._quote_yaml_value(reference.peer_review_status)}"
|
|
500
|
+
)
|
|
495
501
|
if reference.full_text_attempted:
|
|
496
502
|
lines.append("full_text_attempted: true")
|
|
497
503
|
if reference.full_text_provider:
|
|
@@ -651,6 +657,8 @@ class ReferenceFetcher:
|
|
|
651
657
|
oa_status=frontmatter.get("oa_status"),
|
|
652
658
|
license=frontmatter.get("license"),
|
|
653
659
|
local_pdf_path=frontmatter.get("local_pdf_path"),
|
|
660
|
+
is_preprint=frontmatter.get("is_preprint"),
|
|
661
|
+
peer_review_status=frontmatter.get("peer_review_status"),
|
|
654
662
|
full_text_attempted=bool(frontmatter.get("full_text_attempted", False)),
|
|
655
663
|
)
|
|
656
664
|
|
|
@@ -24,6 +24,7 @@ from linkml_reference_validator.etl.sources.base import (
|
|
|
24
24
|
# Import sources to register them
|
|
25
25
|
from linkml_reference_validator.etl.sources.pmid import PMIDSource
|
|
26
26
|
from linkml_reference_validator.etl.sources.doi import DOISource
|
|
27
|
+
from linkml_reference_validator.etl.sources.ppr import PPRSource
|
|
27
28
|
from linkml_reference_validator.etl.sources.file import FileSource
|
|
28
29
|
from linkml_reference_validator.etl.sources.url import URLSource
|
|
29
30
|
from linkml_reference_validator.etl.sources.entrez import (
|
|
@@ -50,6 +51,7 @@ __all__ = [
|
|
|
50
51
|
"ReferenceSourceRegistry",
|
|
51
52
|
"PMIDSource",
|
|
52
53
|
"DOISource",
|
|
54
|
+
"PPRSource",
|
|
53
55
|
"FileSource",
|
|
54
56
|
"URLSource",
|
|
55
57
|
"GEOSource",
|
|
@@ -140,6 +140,8 @@ class DOISource(ReferenceSource):
|
|
|
140
140
|
abstract = (abstract + "\n\n" + format_extra_fields_for_content(extra)) if abstract else format_extra_fields_for_content(extra)
|
|
141
141
|
metadata["extra_fields_captured"] = list(extra.keys())
|
|
142
142
|
|
|
143
|
+
is_preprint = self._crossref_preprint_status(message)
|
|
144
|
+
|
|
143
145
|
return ReferenceContent(
|
|
144
146
|
reference_id=f"DOI:{doi}",
|
|
145
147
|
title=title,
|
|
@@ -151,8 +153,40 @@ class DOISource(ReferenceSource):
|
|
|
151
153
|
doi=doi,
|
|
152
154
|
keywords=keywords,
|
|
153
155
|
metadata=metadata,
|
|
156
|
+
is_preprint=is_preprint,
|
|
157
|
+
peer_review_status="preprint" if is_preprint else None,
|
|
154
158
|
)
|
|
155
159
|
|
|
160
|
+
def _crossref_preprint_status(self, message: dict) -> Optional[bool]:
|
|
161
|
+
"""Classify a Crossref work as preprint / not-preprint / unknown.
|
|
162
|
+
|
|
163
|
+
Crossref models preprints as ``type: "posted-content"`` (usually with
|
|
164
|
+
``subtype: "preprint"``). The ``type`` is authoritative, so a known
|
|
165
|
+
non-preprint type returns ``False`` (recorded so a preprint-specific
|
|
166
|
+
full-text provider can skip the record without a wasted lookup). Only an
|
|
167
|
+
entirely typeless record is left ``None`` ("unknown"). A DOI-prefix
|
|
168
|
+
heuristic is deliberately not used because, e.g., the ``10.1101`` prefix
|
|
169
|
+
also covers peer-reviewed Cold Spring Harbor journals.
|
|
170
|
+
|
|
171
|
+
Examples:
|
|
172
|
+
>>> source = DOISource()
|
|
173
|
+
>>> source._crossref_preprint_status({"type": "posted-content", "subtype": "preprint"})
|
|
174
|
+
True
|
|
175
|
+
>>> source._crossref_preprint_status({"type": "posted-content"})
|
|
176
|
+
True
|
|
177
|
+
>>> source._crossref_preprint_status({"type": "journal-article"})
|
|
178
|
+
False
|
|
179
|
+
>>> source._crossref_preprint_status({}) is None
|
|
180
|
+
True
|
|
181
|
+
"""
|
|
182
|
+
if message.get("type") == "posted-content":
|
|
183
|
+
return True
|
|
184
|
+
if str(message.get("subtype", "")).lower() == "preprint":
|
|
185
|
+
return True
|
|
186
|
+
if message.get("type"):
|
|
187
|
+
return False
|
|
188
|
+
return None
|
|
189
|
+
|
|
156
190
|
def _fetch_from_datacite(
|
|
157
191
|
self, doi: str, config: ReferenceValidationConfig
|
|
158
192
|
) -> Optional[ReferenceContent]:
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
"""Preprint (Europe PMC ``SRC:PPR``) reference source.
|
|
2
|
+
|
|
3
|
+
Resolves a preprint by its Europe PMC preprint id (``PPR:PPR123456``) to metadata
|
|
4
|
+
via the Europe PMC REST search API. The ``SRC:PPR`` universe is the preprint
|
|
5
|
+
firehose: DOI-bearing but without PMIDs, so these records are not reachable
|
|
6
|
+
through the normal PMID path. Records resolved here are always marked as
|
|
7
|
+
preprints (``is_preprint`` / ``peer_review_status``) so downstream knowledge bases
|
|
8
|
+
can apply "not sole support" policies.
|
|
9
|
+
|
|
10
|
+
Preprint *full text* is fetched separately by the ``epmc_preprint`` full-text
|
|
11
|
+
provider (via the DOI/PPR id crosswalk); this source only supplies metadata and
|
|
12
|
+
the abstract.
|
|
13
|
+
|
|
14
|
+
Examples:
|
|
15
|
+
>>> from linkml_reference_validator.etl.sources.ppr import PPRSource
|
|
16
|
+
>>> PPRSource.prefix()
|
|
17
|
+
'PPR'
|
|
18
|
+
>>> PPRSource.can_handle("PPR:PPR123456")
|
|
19
|
+
True
|
|
20
|
+
>>> PPRSource.can_handle("DOI:10.1101/x")
|
|
21
|
+
False
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
import logging
|
|
25
|
+
import time
|
|
26
|
+
from typing import Optional
|
|
27
|
+
|
|
28
|
+
import requests # type: ignore
|
|
29
|
+
|
|
30
|
+
from linkml_reference_validator.models import ReferenceContent, ReferenceValidationConfig
|
|
31
|
+
from linkml_reference_validator.etl.sources.base import ReferenceSource, ReferenceSourceRegistry
|
|
32
|
+
|
|
33
|
+
logger = logging.getLogger(__name__)
|
|
34
|
+
|
|
35
|
+
_EPMC_SEARCH_URL = "https://www.ebi.ac.uk/europepmc/webservices/rest/search"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@ReferenceSourceRegistry.register
|
|
39
|
+
class PPRSource(ReferenceSource):
|
|
40
|
+
"""Fetch preprint metadata from Europe PMC by ``SRC:PPR`` id.
|
|
41
|
+
|
|
42
|
+
Examples:
|
|
43
|
+
>>> source = PPRSource()
|
|
44
|
+
>>> source.prefix()
|
|
45
|
+
'PPR'
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
@classmethod
|
|
49
|
+
def prefix(cls) -> str:
|
|
50
|
+
"""Return 'PPR' prefix.
|
|
51
|
+
|
|
52
|
+
Examples:
|
|
53
|
+
>>> PPRSource.prefix()
|
|
54
|
+
'PPR'
|
|
55
|
+
"""
|
|
56
|
+
return "PPR"
|
|
57
|
+
|
|
58
|
+
def fetch(
|
|
59
|
+
self, identifier: str, config: ReferenceValidationConfig
|
|
60
|
+
) -> Optional[ReferenceContent]:
|
|
61
|
+
"""Fetch preprint metadata for a Europe PMC preprint id.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
identifier: Preprint id (with or without the leading ``PPR``).
|
|
65
|
+
config: Configuration including rate limiting and email.
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
ReferenceContent marked as a preprint, or None if not found.
|
|
69
|
+
"""
|
|
70
|
+
ppr_id = self._normalize_id(identifier)
|
|
71
|
+
time.sleep(config.rate_limit_delay)
|
|
72
|
+
|
|
73
|
+
params = {
|
|
74
|
+
"query": f"EXT_ID:{ppr_id} AND SRC:PPR",
|
|
75
|
+
"format": "json",
|
|
76
|
+
"resultType": "core",
|
|
77
|
+
"pageSize": "1",
|
|
78
|
+
"email": config.email,
|
|
79
|
+
}
|
|
80
|
+
# External system boundary: a network blip or a non-JSON body must yield a
|
|
81
|
+
# graceful skip (None) for this one reference, as the PMID and
|
|
82
|
+
# ClinicalTrials sources do, rather than aborting the whole validation run.
|
|
83
|
+
try:
|
|
84
|
+
response = requests.get(_EPMC_SEARCH_URL, params=params, timeout=30)
|
|
85
|
+
if response.status_code != 200:
|
|
86
|
+
logger.warning(f"Europe PMC returned {response.status_code} for PPR:{ppr_id}")
|
|
87
|
+
return None
|
|
88
|
+
data = response.json()
|
|
89
|
+
except Exception as exc:
|
|
90
|
+
logger.warning(f"Failed to fetch PPR:{ppr_id} from Europe PMC: {exc}")
|
|
91
|
+
return None
|
|
92
|
+
|
|
93
|
+
result = self._first_ppr_result(data)
|
|
94
|
+
if result is None:
|
|
95
|
+
logger.warning(f"No Europe PMC preprint found for PPR:{ppr_id}")
|
|
96
|
+
return None
|
|
97
|
+
|
|
98
|
+
abstract = result.get("abstractText") or None
|
|
99
|
+
doi = result.get("doi") or None
|
|
100
|
+
year = str(result.get("pubYear")) if result.get("pubYear") else None
|
|
101
|
+
|
|
102
|
+
return ReferenceContent(
|
|
103
|
+
reference_id=f"PPR:{ppr_id}",
|
|
104
|
+
title=result.get("title") or None,
|
|
105
|
+
content=abstract,
|
|
106
|
+
content_type="abstract_only" if abstract else "unavailable",
|
|
107
|
+
authors=self._parse_authors(result.get("authorString")),
|
|
108
|
+
journal=self._extract_journal(result),
|
|
109
|
+
year=year,
|
|
110
|
+
doi=doi,
|
|
111
|
+
is_preprint=True,
|
|
112
|
+
peer_review_status="preprint",
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
def _normalize_id(self, identifier: str) -> str:
|
|
116
|
+
"""Normalize a preprint id to Europe PMC's ``PPR`` form.
|
|
117
|
+
|
|
118
|
+
Examples:
|
|
119
|
+
>>> source = PPRSource()
|
|
120
|
+
>>> source._normalize_id("PPR123456")
|
|
121
|
+
'PPR123456'
|
|
122
|
+
>>> source._normalize_id("123456")
|
|
123
|
+
'PPR123456'
|
|
124
|
+
>>> source._normalize_id("ppr123456")
|
|
125
|
+
'PPR123456'
|
|
126
|
+
"""
|
|
127
|
+
stripped = identifier.strip()
|
|
128
|
+
if stripped.upper().startswith("PPR"):
|
|
129
|
+
return "PPR" + stripped[3:]
|
|
130
|
+
return f"PPR{stripped}"
|
|
131
|
+
|
|
132
|
+
def _first_ppr_result(self, data: dict) -> Optional[dict]:
|
|
133
|
+
"""Return the first ``SRC:PPR`` result, or None."""
|
|
134
|
+
results = data.get("resultList", {}).get("result", [])
|
|
135
|
+
for result in results:
|
|
136
|
+
if isinstance(result, dict) and result.get("source") == "PPR":
|
|
137
|
+
return result
|
|
138
|
+
return None
|
|
139
|
+
|
|
140
|
+
def _parse_authors(self, author_string: Optional[str]) -> Optional[list[str]]:
|
|
141
|
+
"""Split a Europe PMC ``authorString`` into individual names.
|
|
142
|
+
|
|
143
|
+
Examples:
|
|
144
|
+
>>> source = PPRSource()
|
|
145
|
+
>>> source._parse_authors("Smith J, Doe A.")
|
|
146
|
+
['Smith J', 'Doe A']
|
|
147
|
+
>>> source._parse_authors(None) is None
|
|
148
|
+
True
|
|
149
|
+
"""
|
|
150
|
+
if not author_string:
|
|
151
|
+
return None
|
|
152
|
+
names = [name.strip().rstrip(".").strip() for name in author_string.split(",")]
|
|
153
|
+
names = [name for name in names if name]
|
|
154
|
+
return names or None
|
|
155
|
+
|
|
156
|
+
def _extract_journal(self, result: dict) -> Optional[str]:
|
|
157
|
+
"""Extract the hosting server name (e.g. bioRxiv) from journalInfo."""
|
|
158
|
+
journal = result.get("journalInfo", {}).get("journal", {}).get("title")
|
|
159
|
+
return journal or None
|
|
@@ -462,11 +462,12 @@ class ReferenceValidationConfig(BaseModel):
|
|
|
462
462
|
),
|
|
463
463
|
)
|
|
464
464
|
full_text_providers: list[str] = Field(
|
|
465
|
-
default_factory=lambda: ["pmc", "unpaywall", "openalex"],
|
|
465
|
+
default_factory=lambda: ["pmc", "epmc_preprint", "unpaywall", "openalex"],
|
|
466
466
|
description=(
|
|
467
467
|
"Ordered list of full-text provider names to try until one yields usable "
|
|
468
|
-
"full text. Names map to built-in providers (pmc, unpaywall,
|
|
469
|
-
"custom providers loaded from YAML."
|
|
468
|
+
"full text. Names map to built-in providers (pmc, epmc_preprint, unpaywall, "
|
|
469
|
+
"openalex) or custom providers loaded from YAML. The epmc_preprint provider "
|
|
470
|
+
"fetches preprint body text via the Europe PMC fulltextRepo PDF route."
|
|
470
471
|
),
|
|
471
472
|
)
|
|
472
473
|
pdf_backend: str = Field(
|
|
@@ -625,12 +626,19 @@ class ReferenceIdentifiers:
|
|
|
625
626
|
'10.1038/x'
|
|
626
627
|
>>> ids.pmcid is None
|
|
627
628
|
True
|
|
629
|
+
>>> ReferenceIdentifiers(pprid="PPR123456").pprid
|
|
630
|
+
'PPR123456'
|
|
628
631
|
"""
|
|
629
632
|
|
|
630
633
|
doi: Optional[str] = None
|
|
631
634
|
pmid: Optional[str] = None
|
|
632
635
|
pmcid: Optional[str] = None
|
|
633
636
|
url: Optional[str] = None
|
|
637
|
+
pprid: Optional[str] = None # Europe PMC preprint id (SRC:PPR), e.g. "PPR123456"
|
|
638
|
+
# Carried from the metadata source so a preprint-specific provider can skip a
|
|
639
|
+
# record already known to be peer-reviewed (False) without a wasted lookup.
|
|
640
|
+
# None means "unknown", which providers must still attempt.
|
|
641
|
+
is_preprint: Optional[bool] = None
|
|
634
642
|
|
|
635
643
|
|
|
636
644
|
@dataclass
|
|
@@ -679,6 +687,16 @@ class ReferenceContent:
|
|
|
679
687
|
... )
|
|
680
688
|
>>> len(ref.supplementary_files)
|
|
681
689
|
1
|
|
690
|
+
>>> ref = ReferenceContent(
|
|
691
|
+
... reference_id="DOI:10.1101/2024.01.01.573333",
|
|
692
|
+
... title="A preprint",
|
|
693
|
+
... is_preprint=True,
|
|
694
|
+
... peer_review_status="preprint",
|
|
695
|
+
... )
|
|
696
|
+
>>> ref.is_preprint
|
|
697
|
+
True
|
|
698
|
+
>>> ref.peer_review_status
|
|
699
|
+
'preprint'
|
|
682
700
|
"""
|
|
683
701
|
|
|
684
702
|
reference_id: str
|
|
@@ -697,6 +715,13 @@ class ReferenceContent:
|
|
|
697
715
|
oa_status: Optional[str] = None
|
|
698
716
|
license: Optional[str] = None
|
|
699
717
|
local_pdf_path: Optional[str] = None
|
|
718
|
+
# Preprint / peer-review status, surfaced so downstream KBs can apply policies
|
|
719
|
+
# such as "a preprint may not be the sole support for a claim". Left as None
|
|
720
|
+
# when the publication type is unknown; only asserted when positively detected
|
|
721
|
+
# (e.g. Crossref type "posted-content"/subtype "preprint", or a Europe PMC
|
|
722
|
+
# SRC:PPR record).
|
|
723
|
+
is_preprint: Optional[bool] = None
|
|
724
|
+
peer_review_status: Optional[str] = None # e.g. "preprint", "peer_reviewed"
|
|
700
725
|
# True once the full-text chain has been run to a clean (error-free) conclusion
|
|
701
726
|
# for this record. Distinguishes "the abstract is all that exists" from "we
|
|
702
727
|
# haven't successfully tried yet", so a transient outage isn't cached forever.
|